From bd6fb2ece8091ac11b98ec6377f07bfedd4b27bc Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Thu, 2 Nov 2023 11:04:36 +0200 Subject: [PATCH 01/67] wip columns selector --- grafana-plugin/src/assets/style/utils.css | 27 +++++++ .../CustomContextMenuDisplay.tsx | 49 ++++++++++++ .../WithContextMenu/WithContextMenu.tsx | 3 + .../src/pages/incidents/Incidents.module.scss | 21 ++++- .../src/pages/incidents/Incidents.tsx | 80 +++++++++++++++++-- .../outgoing_webhooks/OutgoingWebhooks.tsx | 14 +++- 6 files changed, 186 insertions(+), 8 deletions(-) create mode 100644 grafana-plugin/src/components/CustomContextMenuDisplay/CustomContextMenuDisplay.tsx diff --git a/grafana-plugin/src/assets/style/utils.css b/grafana-plugin/src/assets/style/utils.css index bbcc0df930..30fc55febb 100644 --- a/grafana-plugin/src/assets/style/utils.css +++ b/grafana-plugin/src/assets/style/utils.css @@ -150,3 +150,30 @@ .line-clamp-3 { -webkit-line-clamp: 3; } + +/* ---- + * Hamburger Menu + */ + +.hamburgerMenu { + cursor: pointer; + color: var(--primary-text-color); + display: inline-flex; + flex-direction: column; + align-items: center; + vertical-align: middle; + justify-content: center; + padding: 4px; + + &--withBackground { + height: 32px; + width: 30px; + cursor: pointer; + } + + &--small { + height: 24px; + width: 22px; + cursor: pointer; + } +} diff --git a/grafana-plugin/src/components/CustomContextMenuDisplay/CustomContextMenuDisplay.tsx b/grafana-plugin/src/components/CustomContextMenuDisplay/CustomContextMenuDisplay.tsx new file mode 100644 index 0000000000..0abe1cf760 --- /dev/null +++ b/grafana-plugin/src/components/CustomContextMenuDisplay/CustomContextMenuDisplay.tsx @@ -0,0 +1,49 @@ +import React, { useRef } from 'react'; + +interface CustomContextMenuDisplayProps { + openMenu: React.MouseEventHandler; + listWidth: number; + listBorder: number; + stopPropagation?: boolean; + withBackground?: boolean; + baseClassName?: string; + extraClassName?: string; + children: React.ReactNode; +} + +const CustomContextMenuDisplay: React.FC = (props) => { + const ref = useRef(); + const { + openMenu, + children, + listWidth, + listBorder, + withBackground, + baseClassName, + extraClassName, + stopPropagation = false, + } = props; + + return ( +
{ + if (stopPropagation) { + e.stopPropagation(); + } + + const boundingRect = ref.current.getBoundingClientRect(); + + openMenu({ + pageX: boundingRect.right - listWidth + listBorder * 2, + pageY: boundingRect.top + boundingRect.height, + } as any); + }} + > + {children} +
+ ); +}; + +export default CustomContextMenuDisplay; diff --git a/grafana-plugin/src/components/WithContextMenu/WithContextMenu.tsx b/grafana-plugin/src/components/WithContextMenu/WithContextMenu.tsx index a5fe4ab2c7..8a27ba357c 100644 --- a/grafana-plugin/src/components/WithContextMenu/WithContextMenu.tsx +++ b/grafana-plugin/src/components/WithContextMenu/WithContextMenu.tsx @@ -40,6 +40,9 @@ export const WithContextMenu: React.FC = ({
{children({ openMenu: (e) => { + console.log('Should trigger'); + console.log({ x: e.pageX, y: e.pageY }); + setIsMenuOpen(true); setMenuPosition({ x: e.pageX, diff --git a/grafana-plugin/src/pages/incidents/Incidents.module.scss b/grafana-plugin/src/pages/incidents/Incidents.module.scss index b540eee5ee..e046bc787f 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.module.scss +++ b/grafana-plugin/src/pages/incidents/Incidents.module.scss @@ -17,8 +17,15 @@ align-items: center; } -.bulk-actions { +.bulk-actions-container { margin: 10px 0 10px 0; + display: flex; + width: 100%; +} +.bulk-actions-list { + display: flex; + align-items: center; + gap: 8px; } .other-users { @@ -76,3 +83,15 @@ max-width: 100%; } } + +.columns-selector-container { + margin-left: auto; +} + +.columns-selector-view {} +.columns-visible-section {} +.columns-hidden-section {} +.columns-selector-buttons { + display: flex; + align-items: flex-end; +} \ No newline at end of file diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index 8dc34b7dca..045fd659e4 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -1,6 +1,6 @@ -import React, { SyntheticEvent } from 'react'; +import React, { SyntheticEvent, useState } from 'react'; -import { Button, HorizontalGroup, Icon, VerticalGroup } from '@grafana/ui'; +import { Button, Checkbox, HorizontalGroup, Icon, VerticalGroup, WithContextMenu } from '@grafana/ui'; import cn from 'classnames/bind'; import { observer } from 'mobx-react'; import moment from 'moment-timezone'; @@ -32,6 +32,7 @@ import { PAGE, PLUGIN_ROOT, TEXT_ELLIPSIS_CLASS } from 'utils/consts'; import styles from './Incidents.module.scss'; import { IncidentDropdown } from './parts/IncidentDropdown'; import { SilenceButtonCascader } from './parts/SilenceButtonCascader'; +import CustomContextMenuDisplay from 'components/CustomContextMenuDisplay/CustomContextMenuDisplay'; const cx = cn.bind(styles); @@ -371,8 +372,8 @@ class Incidents extends React.Component return (
-
- +
+
{'resolve' in store.alertGroupStore.bulkActions && (
+ +
+ }> + {({ openMenu }) => ( + + + + )} + +
+ {hasInvalidatedAlert && (
Results out of date @@ -757,4 +780,51 @@ class Incidents extends React.Component } } +interface Column { + name: string; + isHidden?: boolean; + icon?: string; +} + +const startingColumnsData: Column[] = [ + { name: 'Status' }, + { name: 'ID' }, + { name: 'Summary' }, + { name: 'Integration' }, + { name: 'Users' }, + { name: 'Team' }, + { name: 'Cortex' }, + { name: 'Created' }, +]; + +const ColumnSelector: React.FC = () => { + const [columns, _setColumns] = useState(startingColumnsData); + + return ( +
+ Fields Settings + +
+ Visible + {columns.map((col) => ( + + + + ))} +
+ +
+ Hidden +
+ +
+ + +
+
+ ); +}; + export default withRouter(withMobXProviderContext(Incidents)); diff --git a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx index 825a423287..bd3292355f 100644 --- a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx +++ b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx @@ -18,7 +18,6 @@ import CopyToClipboard from 'react-copy-to-clipboard'; import { RouteComponentProps, withRouter } from 'react-router-dom'; import GTable from 'components/GTable/GTable'; -import HamburgerMenu from 'components/HamburgerMenu/HamburgerMenu'; import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper'; import { getWrongTeamResponseInfo, @@ -41,6 +40,7 @@ import { PAGE, PLUGIN_ROOT, TEXT_ELLIPSIS_CLASS } from 'utils/consts'; import styles from './OutgoingWebhooks.module.scss'; import { WebhookFormActionType } from './OutgoingWebhooks.types'; +import CustomContextMenuDisplay from 'components/CustomContextMenuDisplay/CustomContextMenuDisplay'; const cx = cn.bind(styles); @@ -332,7 +332,17 @@ class OutgoingWebhooks extends React.Component )} > - {({ openMenu }) => } + {({ openMenu }) => ( + + + + )} ); }; From 8070b0d3ed2ebb18d6838d6008ec4a4e34b7d68a Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Thu, 2 Nov 2023 18:15:58 +0200 Subject: [PATCH 02/67] WIP drag&drop on the columns --- grafana-plugin/package.json | 5 +- .../src/components/CheatSheet/CheatSheet.tsx | 4 +- .../IntegrationCollapsibleTreeView.tsx | 2 +- .../IntegrationContactPoint.tsx | 2 + .../IntegrationInputField.tsx | 6 +- .../components/Policy/NotificationPolicy.tsx | 1 + .../ScheduleQualityDetails.tsx | 6 +- .../src/components/SourceCode/SourceCode.tsx | 2 +- .../UserGroups/UserGroups.module.css | 12 - .../src/components/UserGroups/UserGroups.tsx | 9 +- .../WithContextMenu/WithContextMenu.tsx | 17 +- .../IncidentsFilters/IncidentsFilters.tsx | 2 +- .../containers/RotationForm/RotationForm.tsx | 2 +- .../RotationForm/ScheduleOverrideForm.tsx | 2 +- .../containers/RotationForm/ShiftSwapForm.tsx | 2 +- .../src/containers/TeamsList/TeamsList.tsx | 2 +- .../TemplatesAlertGroupsList.tsx | 8 +- .../src/pages/incident/Incident.tsx | 4 +- .../src/pages/incidents/ColumnsSelector.tsx | 79 ++++ .../src/pages/incidents/Incidents.module.scss | 32 +- .../src/pages/incidents/Incidents.tsx | 81 +--- .../src/pages/integration/Integration.tsx | 2 +- .../src/pages/schedule/Schedule.tsx | 2 +- .../src/pages/schedules/Schedules.module.css | 4 + grafana-plugin/yarn.lock | 404 ++++++++++++------ 25 files changed, 458 insertions(+), 234 deletions(-) create mode 100644 grafana-plugin/src/pages/incidents/ColumnsSelector.tsx diff --git a/grafana-plugin/package.json b/grafana-plugin/package.json index e3b7d1c1e3..3a20c29cf4 100644 --- a/grafana-plugin/package.json +++ b/grafana-plugin/package.json @@ -110,12 +110,15 @@ "node": ">=14" }, "dependencies": { + "@dnd-kit/core": "^6.0.8", + "@dnd-kit/sortable": "^7.0.2", + "@dnd-kit/utilities": "^3.2.1", "@grafana/data": "^9.2.4", "@grafana/faro-web-sdk": "^1.0.0-beta4", "@grafana/faro-web-tracing": "^1.0.0-beta4", "@grafana/labels": "~1.2.1", "@grafana/runtime": "9.3.0-beta1", - "@grafana/ui": "^9.4.7", + "@grafana/ui": "^10.2.0", "@opentelemetry/api": "^1.3.0", "array-move": "^4.0.0", "change-case": "^4.1.1", diff --git a/grafana-plugin/src/components/CheatSheet/CheatSheet.tsx b/grafana-plugin/src/components/CheatSheet/CheatSheet.tsx index 85329a4fbd..194f70ec51 100644 --- a/grafana-plugin/src/components/CheatSheet/CheatSheet.tsx +++ b/grafana-plugin/src/components/CheatSheet/CheatSheet.tsx @@ -27,7 +27,7 @@ const CheatSheet = (props: CheatSheetProps) => { {cheatSheetName} cheatsheet - + {cheatSheetData.description}
@@ -70,7 +70,7 @@ const CheatSheetListItem = (props: CheatSheetListItemProps) => { {item.codeExample} openNotification('Example copied')}> - + diff --git a/grafana-plugin/src/components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView.tsx b/grafana-plugin/src/components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView.tsx index 97c0e7c496..971f132095 100644 --- a/grafana-plugin/src/components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView.tsx +++ b/grafana-plugin/src/components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView.tsx @@ -112,7 +112,7 @@ const IntegrationCollapsibleTreeItem: React.FC<{
{item.canHoverIcon ? ( - + ) : ( )} diff --git a/grafana-plugin/src/components/IntegrationContactPoint/IntegrationContactPoint.tsx b/grafana-plugin/src/components/IntegrationContactPoint/IntegrationContactPoint.tsx index 7454403a72..8eea8856dd 100644 --- a/grafana-plugin/src/components/IntegrationContactPoint/IntegrationContactPoint.tsx +++ b/grafana-plugin/src/components/IntegrationContactPoint/IntegrationContactPoint.tsx @@ -256,6 +256,7 @@ const IntegrationContactPoint: React.FC<{ return ( { window.open( @@ -277,6 +278,7 @@ const IntegrationContactPoint: React.FC<{ } > { alertReceiveChannelStore diff --git a/grafana-plugin/src/components/IntegrationInputField/IntegrationInputField.tsx b/grafana-plugin/src/components/IntegrationInputField/IntegrationInputField.tsx index b255fa6073..8047ad542c 100644 --- a/grafana-plugin/src/components/IntegrationInputField/IntegrationInputField.tsx +++ b/grafana-plugin/src/components/IntegrationInputField/IntegrationInputField.tsx @@ -35,13 +35,13 @@ const IntegrationInputField: React.FC = ({
- {showEye && } + {showEye && } {showCopy && ( - + )} - {showExternal && } + {showExternal && }
diff --git a/grafana-plugin/src/components/Policy/NotificationPolicy.tsx b/grafana-plugin/src/components/Policy/NotificationPolicy.tsx index fd90261b84..4a6c9d84f7 100644 --- a/grafana-plugin/src/components/Policy/NotificationPolicy.tsx +++ b/grafana-plugin/src/components/Policy/NotificationPolicy.tsx @@ -72,6 +72,7 @@ export class NotificationPolicy extends React.Component = ({ qualit Calculation methodology - + {expanded && ( diff --git a/grafana-plugin/src/components/SourceCode/SourceCode.tsx b/grafana-plugin/src/components/SourceCode/SourceCode.tsx index ecc27c61f1..de316ee65e 100644 --- a/grafana-plugin/src/components/SourceCode/SourceCode.tsx +++ b/grafana-plugin/src/components/SourceCode/SourceCode.tsx @@ -33,7 +33,7 @@ const SourceCode: FC = (props) => { > {showClipboardIconOnly ? ( - + ) : (
diff --git a/grafana-plugin/src/pages/incidents/ColumnsSelector.tsx b/grafana-plugin/src/pages/incidents/ColumnsSelector.tsx index e4d4698c25..dc6cad96a5 100644 --- a/grafana-plugin/src/pages/incidents/ColumnsSelector.tsx +++ b/grafana-plugin/src/pages/incidents/ColumnsSelector.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import cn from 'classnames/bind'; + import { DndContext, closestCenter, @@ -16,12 +16,12 @@ import { verticalListSortingStrategy, useSortable, } from '@dnd-kit/sortable'; - import { CSS } from '@dnd-kit/utilities'; +import { Button, Checkbox, IconButton } from '@grafana/ui'; +import cn from 'classnames/bind'; -import styles from 'pages/incidents/ColumnsSelector.module.scss'; import Text from 'components/Text/Text'; -import { Button, Checkbox, IconButton } from '@grafana/ui'; +import styles from 'pages/incidents/ColumnsSelector.module.scss'; const cx = cn.bind(styles); diff --git a/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.tsx b/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.tsx index 58881d68a5..72beb5d59a 100644 --- a/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.tsx +++ b/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.tsx @@ -1,5 +1,10 @@ -import { Button, HorizontalGroup, Icon, Modal, Toggletip } from '@grafana/ui'; import React, { useState } from 'react'; + +import { Button, HorizontalGroup, Icon, Input, Modal, Toggletip, VerticalGroup } from '@grafana/ui'; +import { noop } from 'lodash-es'; + +import Text from 'components/Text/Text'; + import { ColumnsSelector } from './ColumnsSelector'; interface ColumnsSelectorWrapperProps {} @@ -10,10 +15,16 @@ const ColumnsSelectorWrapper: React.FC = () => { return ( <> setIsModalOpen(false)}> - - - - + + + + 2101 items available. Type in to see suggestions + + + + + + {!isModalOpen ? ( diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index d85b53bf3d..7b650c50eb 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -29,12 +29,11 @@ import LocationHelper from 'utils/LocationHelper'; import { UserActions } from 'utils/authorization'; import { PAGE, PLUGIN_ROOT, TEXT_ELLIPSIS_CLASS } from 'utils/consts'; +import ColumnsSelectorWrapper from './ColumnsSelectorWrapper'; import styles from './Incidents.module.scss'; import { IncidentDropdown } from './parts/IncidentDropdown'; import { SilenceButtonCascader } from './parts/SilenceButtonCascader'; -import ColumnsSelectorWrapper from './ColumnsSelectorWrapper'; - const cx = cn.bind(styles); interface Pagination { diff --git a/grafana-plugin/src/pages/integration/Integration.tsx b/grafana-plugin/src/pages/integration/Integration.tsx index 929fa2ca24..501b175927 100644 --- a/grafana-plugin/src/pages/integration/Integration.tsx +++ b/grafana-plugin/src/pages/integration/Integration.tsx @@ -180,7 +180,7 @@ class Integration extends React.Component {
- +

diff --git a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx index bd3292355f..07d2a6259d 100644 --- a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx +++ b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx @@ -17,6 +17,7 @@ import LegacyNavHeading from 'navbar/LegacyNavHeading'; import CopyToClipboard from 'react-copy-to-clipboard'; import { RouteComponentProps, withRouter } from 'react-router-dom'; +import CustomContextMenuDisplay from 'components/CustomContextMenuDisplay/CustomContextMenuDisplay'; import GTable from 'components/GTable/GTable'; import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper'; import { @@ -40,7 +41,6 @@ import { PAGE, PLUGIN_ROOT, TEXT_ELLIPSIS_CLASS } from 'utils/consts'; import styles from './OutgoingWebhooks.module.scss'; import { WebhookFormActionType } from './OutgoingWebhooks.types'; -import CustomContextMenuDisplay from 'components/CustomContextMenuDisplay/CustomContextMenuDisplay'; const cx = cn.bind(styles); diff --git a/grafana-plugin/src/pages/schedule/Schedule.tsx b/grafana-plugin/src/pages/schedule/Schedule.tsx index d092bd1b9b..3381311e0b 100644 --- a/grafana-plugin/src/pages/schedule/Schedule.tsx +++ b/grafana-plugin/src/pages/schedule/Schedule.tsx @@ -185,7 +185,7 @@ class SchedulePage extends React.Component
- + Date: Mon, 6 Nov 2023 17:05:57 +0200 Subject: [PATCH 07/67] removed react-dnd --- grafana-plugin/package.json | 2 - .../src/containers/Labels/Labels.module.css | 2 - grafana-plugin/yarn.lock | 49 ------------------- 3 files changed, 53 deletions(-) delete mode 100644 grafana-plugin/src/containers/Labels/Labels.module.css diff --git a/grafana-plugin/package.json b/grafana-plugin/package.json index 5f65776c28..954cfb7102 100644 --- a/grafana-plugin/package.json +++ b/grafana-plugin/package.json @@ -133,8 +133,6 @@ "raw-loader": "^4.0.2", "rc-table": "^7.17.1", "react-copy-to-clipboard": "^5.0.2", - "react-dnd": "^16.0.1", - "react-dnd-html5-backend": "^16.0.1", "react-draggable": "^4.4.5", "react-emoji-render": "^1.2.4", "react-modal": "^3.15.1", diff --git a/grafana-plugin/src/containers/Labels/Labels.module.css b/grafana-plugin/src/containers/Labels/Labels.module.css deleted file mode 100644 index c3a2af6391..0000000000 --- a/grafana-plugin/src/containers/Labels/Labels.module.css +++ /dev/null @@ -1,2 +0,0 @@ -.root { -} diff --git a/grafana-plugin/yarn.lock b/grafana-plugin/yarn.lock index 8e821ef194..afed9d898c 100644 --- a/grafana-plugin/yarn.lock +++ b/grafana-plugin/yarn.lock @@ -3661,21 +3661,6 @@ "@swc/helpers" "^0.5.0" clsx "^1.1.1" -"@react-dnd/asap@^5.0.1": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-5.0.2.tgz#1f81f124c1cd6f39511c11a881cfb0f715343488" - integrity sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A== - -"@react-dnd/invariant@^4.0.1": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-4.0.2.tgz#b92edffca10a26466643349fac7cdfb8799769df" - integrity sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw== - -"@react-dnd/shallowequal@^4.0.1": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz#d1b4befa423f692fa4abf1c79209702e7d8ae4b4" - integrity sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA== - "@react-stately/collections@^3.10.2", "@react-stately/collections@^3.9.0": version "3.10.2" resolved "https://registry.yarnpkg.com/@react-stately/collections/-/collections-3.10.2.tgz#c739d9d596ecb744be15fde6f064ad85dd6145db" @@ -7314,15 +7299,6 @@ direction@^0.1.5: resolved "https://registry.yarnpkg.com/direction/-/direction-0.1.5.tgz#ce5d797f97e26f8be7beff53f7dc40e1c1a9ec4c" integrity sha512-HceXsAluGbXKCz2qCVbXFUH4Vn4eNMWxY5gzydMFMnS1zKSwvDASqLwcrYLIFDpwuZ63FUAqjDLEP1eicHt8DQ== -dnd-core@^16.0.1: - version "16.0.1" - resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-16.0.1.tgz#a1c213ed08961f6bd1959a28bb76f1a868360d19" - integrity sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng== - dependencies: - "@react-dnd/asap" "^5.0.1" - "@react-dnd/invariant" "^4.0.1" - redux "^4.2.0" - doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -13209,24 +13185,6 @@ react-dev-utils@^12.0.0: strip-ansi "^6.0.1" text-table "^0.2.0" -react-dnd-html5-backend@^16.0.1: - version "16.0.1" - resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz#87faef15845d512a23b3c08d29ecfd34871688b6" - integrity sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw== - dependencies: - dnd-core "^16.0.1" - -react-dnd@^16.0.1: - version "16.0.1" - resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-16.0.1.tgz#2442a3ec67892c60d40a1559eef45498ba26fa37" - integrity sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q== - dependencies: - "@react-dnd/invariant" "^4.0.1" - "@react-dnd/shallowequal" "^4.0.1" - dnd-core "^16.0.1" - fast-deep-equal "^3.1.3" - hoist-non-react-statics "^3.3.2" - react-dom@18.2.0, react-dom@^18.0.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" @@ -13684,13 +13642,6 @@ redux@^4.0.0, redux@^4.0.4: dependencies: "@babel/runtime" "^7.9.2" -redux@^4.2.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" - integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== - dependencies: - "@babel/runtime" "^7.9.2" - regenerate-unicode-properties@^10.1.0: version "10.1.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c" From 35f9dd3813e2d5a481b685b0e9082365932316ef Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 6 Nov 2023 17:09:47 +0200 Subject: [PATCH 08/67] removed unused references --- grafana-plugin/src/containers/Labels/Labels.tsx | 7 +------ grafana-plugin/src/containers/Labels/LabelsFilter.tsx | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/grafana-plugin/src/containers/Labels/Labels.tsx b/grafana-plugin/src/containers/Labels/Labels.tsx index f744e3f926..d7ba71ec26 100644 --- a/grafana-plugin/src/containers/Labels/Labels.tsx +++ b/grafana-plugin/src/containers/Labels/Labels.tsx @@ -2,7 +2,6 @@ import React, { forwardRef, useCallback, useImperativeHandle, useState } from 'r import ServiceLabels from '@grafana/labels'; import { Field } from '@grafana/ui'; -import cn from 'classnames/bind'; import { isEmpty } from 'lodash-es'; import { observer } from 'mobx-react'; @@ -10,10 +9,6 @@ import { LabelKeyValue } from 'models/label/label.types'; import { useStore } from 'state/useStore'; import { openErrorNotification } from 'utils'; -import styles from './Labels.module.css'; - -const cx = cn.bind(styles); - interface LabelsProps { value: LabelKeyValue[]; errors: any; @@ -99,7 +94,7 @@ const Labels = observer( }, []); return ( -
+
{ }; return ( -
+
Date: Tue, 7 Nov 2023 10:26:28 +0200 Subject: [PATCH 09/67] reverted version to 17.0.2 --- grafana-plugin/package.json | 4 +-- grafana-plugin/src/assets/style/utils.css | 35 ----------------------- 2 files changed, 2 insertions(+), 37 deletions(-) diff --git a/grafana-plugin/package.json b/grafana-plugin/package.json index 954cfb7102..c0e81e5e6f 100644 --- a/grafana-plugin/package.json +++ b/grafana-plugin/package.json @@ -96,8 +96,8 @@ "moment-timezone": "^0.5.35", "plop": "^2.7.4", "postcss-loader": "^7.0.1", - "react": "18.2.0", - "react-dom": "18.2.0", + "react": "17.0.2", + "react-dom": "17.0.2", "react-test-renderer": "^17.0.2", "stylelint-config-prettier": "^9.0.3", "stylelint-prettier": "^2.0.0", diff --git a/grafana-plugin/src/assets/style/utils.css b/grafana-plugin/src/assets/style/utils.css index 762629e5f2..bbcc0df930 100644 --- a/grafana-plugin/src/assets/style/utils.css +++ b/grafana-plugin/src/assets/style/utils.css @@ -150,38 +150,3 @@ .line-clamp-3 { -webkit-line-clamp: 3; } - -/* ---- - * Hamburger Menu - */ - -.hamburgerMenu { - cursor: pointer; - color: var(--primary-text-color); - display: inline-flex; - flex-direction: column; - align-items: center; - vertical-align: middle; - justify-content: center; - padding: 4px; - - &--withBackground { - height: 32px; - width: 30px; - cursor: pointer; - } - - &--small { - height: 24px; - width: 22px; - cursor: pointer; - } -} - -.sortableHelper { - z-index: 9990; -} - -.sortable-wrapper { - min-width: 300px; -} From 2a75d2c5cfcad80334bcccce81e5b92cab6c431f Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Tue, 7 Nov 2023 10:26:53 +0200 Subject: [PATCH 10/67] removed unused menuDisplay --- .../CustomContextMenuDisplay.tsx | 49 ------------------- .../outgoing_webhooks/OutgoingWebhooks.tsx | 14 +----- grafana-plugin/yarn.lock | 21 +++++++- 3 files changed, 21 insertions(+), 63 deletions(-) delete mode 100644 grafana-plugin/src/components/CustomContextMenuDisplay/CustomContextMenuDisplay.tsx diff --git a/grafana-plugin/src/components/CustomContextMenuDisplay/CustomContextMenuDisplay.tsx b/grafana-plugin/src/components/CustomContextMenuDisplay/CustomContextMenuDisplay.tsx deleted file mode 100644 index 0abe1cf760..0000000000 --- a/grafana-plugin/src/components/CustomContextMenuDisplay/CustomContextMenuDisplay.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React, { useRef } from 'react'; - -interface CustomContextMenuDisplayProps { - openMenu: React.MouseEventHandler; - listWidth: number; - listBorder: number; - stopPropagation?: boolean; - withBackground?: boolean; - baseClassName?: string; - extraClassName?: string; - children: React.ReactNode; -} - -const CustomContextMenuDisplay: React.FC = (props) => { - const ref = useRef(); - const { - openMenu, - children, - listWidth, - listBorder, - withBackground, - baseClassName, - extraClassName, - stopPropagation = false, - } = props; - - return ( -
{ - if (stopPropagation) { - e.stopPropagation(); - } - - const boundingRect = ref.current.getBoundingClientRect(); - - openMenu({ - pageX: boundingRect.right - listWidth + listBorder * 2, - pageY: boundingRect.top + boundingRect.height, - } as any); - }} - > - {children} -
- ); -}; - -export default CustomContextMenuDisplay; diff --git a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx index 07d2a6259d..825a423287 100644 --- a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx +++ b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx @@ -17,8 +17,8 @@ import LegacyNavHeading from 'navbar/LegacyNavHeading'; import CopyToClipboard from 'react-copy-to-clipboard'; import { RouteComponentProps, withRouter } from 'react-router-dom'; -import CustomContextMenuDisplay from 'components/CustomContextMenuDisplay/CustomContextMenuDisplay'; import GTable from 'components/GTable/GTable'; +import HamburgerMenu from 'components/HamburgerMenu/HamburgerMenu'; import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper'; import { getWrongTeamResponseInfo, @@ -332,17 +332,7 @@ class OutgoingWebhooks extends React.Component )} > - {({ openMenu }) => ( - - - - )} + {({ openMenu }) => } ); }; diff --git a/grafana-plugin/yarn.lock b/grafana-plugin/yarn.lock index afed9d898c..ea91a24504 100644 --- a/grafana-plugin/yarn.lock +++ b/grafana-plugin/yarn.lock @@ -13185,7 +13185,16 @@ react-dev-utils@^12.0.0: strip-ansi "^6.0.1" text-table "^0.2.0" -react-dom@18.2.0, react-dom@^18.0.0: +react-dom@17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" + integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + scheduler "^0.20.2" + +react-dom@^18.0.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== @@ -13571,7 +13580,15 @@ react-window@1.8.9: "@babel/runtime" "^7.0.0" memoize-one ">=3.1.1 <6" -react@18.2.0, react@^18.0.0: +react@17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" + integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + +react@^18.0.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== From c0dcdc5da7b52ed6d0a1f31d1082277ebb99cb7c Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Tue, 7 Nov 2023 10:32:27 +0200 Subject: [PATCH 11/67] snapshots --- .../__snapshots__/Unauthorized.test.tsx.snap | 30 ++-- .../__snapshots__/AddResponders.test.tsx.snap | 82 +++-------- .../AddRespondersPopup.test.tsx.snap | 90 ++++++------ .../NotificationPoliciesSelect.test.tsx.snap | 52 ++----- .../__snapshots__/TeamResponder.test.tsx.snap | 31 ++-- .../__snapshots__/UserResponder.test.tsx.snap | 63 +++----- .../MobileAppConnection.test.tsx.snap | 136 ++++++++---------- .../DisconnectButton.test.tsx.snap | 4 +- .../__snapshots__/DownloadIcons.test.tsx.snap | 14 +- .../PluginConfigPage.test.tsx.snap | 132 ++++++++--------- .../ConfigurationForm.test.tsx.snap | 88 ++++++------ ...veCurrentConfigurationButton.test.tsx.snap | 8 +- .../__snapshots__/PluginSetup.test.tsx.snap | 28 ++-- 13 files changed, 317 insertions(+), 441 deletions(-) diff --git a/grafana-plugin/src/components/Unauthorized/__snapshots__/Unauthorized.test.tsx.snap b/grafana-plugin/src/components/Unauthorized/__snapshots__/Unauthorized.test.tsx.snap index cc8e393c54..b648c35ec6 100644 --- a/grafana-plugin/src/components/Unauthorized/__snapshots__/Unauthorized.test.tsx.snap +++ b/grafana-plugin/src/components/Unauthorized/__snapshots__/Unauthorized.test.tsx.snap @@ -5,7 +5,7 @@ exports[`Unauthorized renders properly - access control enabled: false 1`] = ` className="not-found" >

Loading...
diff --git a/grafana-plugin/src/containers/AddResponders/parts/NotificationPoliciesSelect/__snapshots__/NotificationPoliciesSelect.test.tsx.snap b/grafana-plugin/src/containers/AddResponders/parts/NotificationPoliciesSelect/__snapshots__/NotificationPoliciesSelect.test.tsx.snap index 1308038d58..a991d6100b 100644 --- a/grafana-plugin/src/containers/AddResponders/parts/NotificationPoliciesSelect/__snapshots__/NotificationPoliciesSelect.test.tsx.snap +++ b/grafana-plugin/src/containers/AddResponders/parts/NotificationPoliciesSelect/__snapshots__/NotificationPoliciesSelect.test.tsx.snap @@ -3,7 +3,7 @@ exports[`NotificationPoliciesSelect disabled state 1`] = `
Default
@@ -41,23 +41,11 @@ exports[`NotificationPoliciesSelect disabled state 1`] = ` />
- - - -
+ class="css-1j2891d-Icon" + />
@@ -67,7 +55,7 @@ exports[`NotificationPoliciesSelect disabled state 1`] = ` exports[`NotificationPoliciesSelect it renders properly 1`] = `
Default
@@ -104,23 +92,11 @@ exports[`NotificationPoliciesSelect it renders properly 1`] = ` />
- - - -
+ class="css-1j2891d-Icon" + />
diff --git a/grafana-plugin/src/containers/AddResponders/parts/TeamResponder/__snapshots__/TeamResponder.test.tsx.snap b/grafana-plugin/src/containers/AddResponders/parts/TeamResponder/__snapshots__/TeamResponder.test.tsx.snap index 420483ecf5..aa6662ce1a 100644 --- a/grafana-plugin/src/containers/AddResponders/parts/TeamResponder/__snapshots__/TeamResponder.test.tsx.snap +++ b/grafana-plugin/src/containers/AddResponders/parts/TeamResponder/__snapshots__/TeamResponder.test.tsx.snap @@ -4,18 +4,18 @@ exports[`TeamResponder it renders data properly 1`] = `
  • diff --git a/grafana-plugin/src/containers/AddResponders/parts/UserResponder/__snapshots__/UserResponder.test.tsx.snap b/grafana-plugin/src/containers/AddResponders/parts/UserResponder/__snapshots__/UserResponder.test.tsx.snap index 2311afb893..7698f4d388 100644 --- a/grafana-plugin/src/containers/AddResponders/parts/UserResponder/__snapshots__/UserResponder.test.tsx.snap +++ b/grafana-plugin/src/containers/AddResponders/parts/UserResponder/__snapshots__/UserResponder.test.tsx.snap @@ -4,18 +4,18 @@ exports[`UserResponder it renders data properly 1`] = `
  • Important
    @@ -86,51 +86,28 @@ exports[`UserResponder it renders data properly 1`] = ` />
    - - - -
    + class="css-1j2891d-Icon" + />
    diff --git a/grafana-plugin/src/containers/MobileAppConnection/__snapshots__/MobileAppConnection.test.tsx.snap b/grafana-plugin/src/containers/MobileAppConnection/__snapshots__/MobileAppConnection.test.tsx.snap index b583c4b8d6..61aaacdbdf 100644 --- a/grafana-plugin/src/containers/MobileAppConnection/__snapshots__/MobileAppConnection.test.tsx.snap +++ b/grafana-plugin/src/containers/MobileAppConnection/__snapshots__/MobileAppConnection.test.tsx.snap @@ -2357,11 +2357,11 @@ exports[`MobileAppConnection if we disconnect the app, it disconnects and fetche exports[`MobileAppConnection it shows a QR code if the app isn't already connected 1`] = `
    Loading...
    @@ -2601,11 +2602,11 @@ exports[`MobileAppConnection it shows a loading message if it is currently disco exports[`MobileAppConnection it shows a loading message if it is currently fetching the QR code 1`] = `
    Loading...
    @@ -2723,11 +2725,11 @@ exports[`MobileAppConnection it shows a loading message if it is currently fetch exports[`MobileAppConnection it shows a message when the mobile app is already connected 1`] = `
    App connected
    - - - -
    + class="css-1j2891d-Icon" + />
    Configure Plugin @@ -124,33 +124,33 @@ exports[`PluginSetup there is an error message 1`] = ` class="configure-plugin" >
    Configure Plugin From 5e44221227b5b79d39aab932f6b028d0a438b43d Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Tue, 7 Nov 2023 11:25:43 +0200 Subject: [PATCH 12/67] mock ResizeObserver, updated packages, updated snapshots --- grafana-plugin/jest.setup.ts | 32 ++ grafana-plugin/package.json | 8 +- .../AddResponders/AddResponders.tsx | 2 + .../__snapshots__/AddResponders.test.tsx.snap | 338 ++++++------------ .../MobileAppConnection.test.tsx.snap | 67 ++-- .../PluginConfigPage.test.tsx.snap | 52 +-- grafana-plugin/yarn.lock | 56 +-- 7 files changed, 228 insertions(+), 327 deletions(-) diff --git a/grafana-plugin/jest.setup.ts b/grafana-plugin/jest.setup.ts index 0dc7419162..1e0ab110e7 100644 --- a/grafana-plugin/jest.setup.ts +++ b/grafana-plugin/jest.setup.ts @@ -19,3 +19,35 @@ Object.defineProperty(window, 'matchMedia', { dispatchEvent: jest.fn(), })), }); + +const global = window as any; + +global.ResizeObserver = class ResizeObserver { + //callback: ResizeObserverCallback; + + constructor(callback: ResizeObserverCallback) { + setTimeout(() => { + callback( + [ + { + contentRect: { + x: 1, + y: 2, + width: 500, + height: 500, + top: 100, + bottom: 0, + left: 100, + right: 0, + }, + target: {}, + } as ResizeObserverEntry, + ], + this + ); + }); + } + observe() {} + disconnect() {} + unobserve() {} +}; \ No newline at end of file diff --git a/grafana-plugin/package.json b/grafana-plugin/package.json index c0e81e5e6f..1cbe479f58 100644 --- a/grafana-plugin/package.json +++ b/grafana-plugin/package.json @@ -75,7 +75,7 @@ "@types/react-dom": "^18.0.6", "@types/react-responsive": "^8.0.5", "@types/react-router-dom": "^5.3.3", - "@types/react-test-renderer": "^17.0.2", + "@types/react-test-renderer": "^18.0.5", "@types/react-transition-group": "^4.4.5", "@types/throttle-debounce": "^5.0.0", "@typescript-eslint/eslint-plugin": "^5.40.1", @@ -96,9 +96,9 @@ "moment-timezone": "^0.5.35", "plop": "^2.7.4", "postcss-loader": "^7.0.1", - "react": "17.0.2", - "react-dom": "17.0.2", - "react-test-renderer": "^17.0.2", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-test-renderer": "^18.0.2", "stylelint-config-prettier": "^9.0.3", "stylelint-prettier": "^2.0.0", "ts-jest": "29.0.3", diff --git a/grafana-plugin/src/containers/AddResponders/AddResponders.tsx b/grafana-plugin/src/containers/AddResponders/AddResponders.tsx index 515bbe7ac1..58e0be88d2 100644 --- a/grafana-plugin/src/containers/AddResponders/AddResponders.tsx +++ b/grafana-plugin/src/containers/AddResponders/AddResponders.tsx @@ -56,6 +56,8 @@ const AddResponders = observer( onAddNewParticipant, generateRemovePreviouslyPagedUserCallback, }: Props) => { + console.log('here'); + const { directPagingStore } = useStore(); const { selectedTeamResponder, selectedUserResponders } = directPagingStore; diff --git a/grafana-plugin/src/containers/AddResponders/__snapshots__/AddResponders.test.tsx.snap b/grafana-plugin/src/containers/AddResponders/__snapshots__/AddResponders.test.tsx.snap index 72b5517c5d..f0795a178e 100644 --- a/grafana-plugin/src/containers/AddResponders/__snapshots__/AddResponders.test.tsx.snap +++ b/grafana-plugin/src/containers/AddResponders/__snapshots__/AddResponders.test.tsx.snap @@ -202,11 +202,11 @@ exports[`AddResponders should render selected team and users properly 1`] = ` class="root root_bordered" >

  • - Choose + Select...
    - - - -
    + class="css-1j2891d-Icon" + />
    @@ -457,18 +411,18 @@ exports[`AddResponders should render selected team and users properly 1`] = `
  • - Choose + Select...
    - - - -
    + class="css-1j2891d-Icon" + />
    @@ -594,18 +525,18 @@ exports[`AddResponders should render selected team and users properly 1`] = `
  • - Choose + Select...
    - - - -
    + class="css-1j2891d-Icon" + />
    @@ -731,81 +639,63 @@ exports[`AddResponders should render selected team and users properly 1`] = `
  • - - - +
    -
    -
    diff --git a/grafana-plugin/src/containers/MobileAppConnection/__snapshots__/MobileAppConnection.test.tsx.snap b/grafana-plugin/src/containers/MobileAppConnection/__snapshots__/MobileAppConnection.test.tsx.snap index 61aaacdbdf..e0e044f3ef 100644 --- a/grafana-plugin/src/containers/MobileAppConnection/__snapshots__/MobileAppConnection.test.tsx.snap +++ b/grafana-plugin/src/containers/MobileAppConnection/__snapshots__/MobileAppConnection.test.tsx.snap @@ -3,11 +3,11 @@ exports[`MobileAppConnection if we disconnect the app, it disconnects and fetches a new QR code 1`] = `
    Loading...
    @@ -2923,11 +2924,11 @@ exports[`MobileAppConnection it shows a warning when cloud is not connected 1`] exports[`MobileAppConnection it shows an error message if there was an error disconnecting the mobile app 1`] = `
    Configure Grafana OnCall @@ -448,32 +448,32 @@ exports[`PluginConfigPage Plugin reset: successful - false 1`] = `
    + @@ -31,7 +85,8 @@ const ColumnsSelectorWrapper: React.FC = () => { setIsModalOpen(!isModalOpen)} />} placement={'bottom-end'} - closeButton={true} + show={true} + closeButton={false} > {renderToggletipButton()} @@ -41,9 +96,18 @@ const ColumnsSelectorWrapper: React.FC = () => { ); + function forceOpenToggletip() { + document.getElementById('toggletip-button')?.click(); + } + + function onInputChange() { + const search = inputRef?.current?.value; + setSearchResults(labelKeys.filter((pair) => pair.name.indexOf(search) > -1)); + } + function renderToggletipButton() { return ( -
    @@ -94,9 +94,9 @@ interface ColumnsSelectorProps { } export const ColumnsSelector: React.FC = ({ onModalOpen }) => { - const [items, setItems] = useState([...startingColumnsData]); - const visibleColumns = items.filter((col) => col.isChecked); - const hiddenColumns = items.filter((col) => !col.isChecked); + const [items, setItems] = useState([...startingColumnsData]); + const visibleColumns = items.filter((col) => col.isVisible); + const hiddenColumns = items.filter((col) => !col.isVisible); const sensors = useSensors( useSensor(PointerSensor), @@ -158,14 +158,14 @@ export const ColumnsSelector: React.FC = ({ onModalOpen }) function onItemChange(id: string | number) { setItems((items) => { - return items.map((it) => (it.id === id ? { ...it, isChecked: !it.isChecked } : it)); + return items.map((it) => (it.id === id ? { ...it, isChecked: !it.isVisible } : it)); }); } function handleDragEnd(event: DragEndEvent, isVisible: boolean) { const { active, over } = event; - let searchableList: Column[] = isVisible ? visibleColumns : hiddenColumns; + let searchableList: AGColumn[] = isVisible ? visibleColumns : hiddenColumns; if (active.id !== over.id) { const oldIndex = searchableList.findIndex((item) => item.id === active.id); diff --git a/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.tsx b/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.tsx index 3de0c5f14e..89b7a8f7f3 100644 --- a/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.tsx +++ b/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.tsx @@ -70,6 +70,9 @@ const ColumnsSelectorWrapper: React.FC = () => {
    - + @@ -156,9 +151,12 @@ export const ColumnsSelector: React.FC = ({ onModalOpen })
    ); + function onReset() {} + function onItemChange(id: string | number) { - setItems((items) => { - return items.map((it) => (it.id === id ? { ...it, isVisible: !it.isVisible } : it)); + alertGroupStore.columns = alertGroupStore.columns.map((item): AGColumn => { + let newItem: AGColumn = { ...item, isVisible: !item.isVisible }; + return item.id === id ? newItem : item; }); } @@ -174,7 +172,7 @@ export const ColumnsSelector: React.FC = ({ onModalOpen }) searchableList = arrayMove(searchableList, oldIndex, newIndex); const updatedList = isVisible ? [...searchableList, ...hiddenColumns] : [...visibleColumns, ...searchableList]; - setItems(updatedList); + alertGroupStore.columns = updatedList; } } -}; +}); diff --git a/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.module.scss b/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.module.scss index c8432b97e6..b2708dcae5 100644 --- a/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.module.scss +++ b/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.module.scss @@ -1,3 +1,3 @@ -.result-spacer { - min-width: 40px; +.input { + margin-bottom: 16px; } diff --git a/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.tsx b/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.tsx index 89b7a8f7f3..6c39c88235 100644 --- a/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.tsx +++ b/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.tsx @@ -8,7 +8,6 @@ import { ColumnsSelector } from './ColumnsSelector'; import { useStore } from 'state/useStore'; import { useDebouncedCallback } from 'utils/hooks'; import { Label } from 'models/label/label.types'; -import { noop } from 'lodash-es'; import cn from 'classnames/bind'; @@ -23,13 +22,10 @@ const DEBOUNCE_MS = 300; const ColumnsSelectorWrapper: React.FC = () => { const [isModalOpen, setIsModalOpen] = useState(false); - const [searchResults, setSearchResults] = useState([]); const [labelKeys, setLabelKeys] = useState([]); const inputRef = useRef(null); - const debouncedOnInputChange = useDebouncedCallback(onInputChange, DEBOUNCE_MS); - const store = useStore(); useEffect(() => { @@ -42,47 +38,12 @@ const ColumnsSelectorWrapper: React.FC = () => { return ( <> - setIsModalOpen(false)}> - - - - {inputRef?.current?.value === '' && ( - {labelKeys.length} items available. Type in to see suggestions - )} - - {searchResults.length && ( - - {searchResults.map((result) => ( - - - -
    - -
    - - {result.name} -
    - ))} -
    - )} - - - - - -
    -
    + {!isModalOpen ? ( = () => { ); - function forceOpenToggletip() { - document.getElementById('toggletip-button')?.click(); - } - - function onInputChange() { - const search = inputRef?.current?.value; - setSearchResults(labelKeys.filter((pair) => pair.name.indexOf(search) > -1)); - } - function renderToggletipButton() { return ( + + + + + ); + + function forceOpenToggletip() { + document.getElementById('toggletip-button')?.click(); + } + + function onInputChange() { + const search = inputRef?.current?.value; + setSearchResults( + labelKeys.filter((pair) => pair.name.indexOf(search) > -1).map((pair) => ({ ...pair, isChecked: false })) + ); + } +}; + export default ColumnsSelectorWrapper; From c3e1b450b998a43603f0bb4156a163f87a7ee233 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Thu, 9 Nov 2023 11:08:19 +0200 Subject: [PATCH 17/67] add new columns from modal actions --- .../incidents/ColumnsSelectorWrapper.tsx | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.tsx b/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.tsx index 6c39c88235..dafce9bc93 100644 --- a/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.tsx +++ b/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.tsx @@ -12,6 +12,7 @@ import { Label } from 'models/label/label.types'; import cn from 'classnames/bind'; import styles from 'pages/incidents/ColumnsSelector.module.scss'; +import { AGColumn } from 'models/alertgroup/alertgroup.types'; const cx = cn.bind(styles); @@ -84,6 +85,7 @@ interface SearchResult extends Label { } const ColumnsModal: React.FC = ({ isModalOpen, labelKeys, setIsModalOpen, inputRef }) => { + const store = useStore(); const [searchResults, setSearchResults] = useState([]); const debouncedOnInputChange = useDebouncedCallback(onInputChange, DEBOUNCE_MS); @@ -142,12 +144,32 @@ const ColumnsModal: React.FC = ({ isModalOpen, labelKeys, set > Close - + ); + function onAddNewColumns() { + const newColumns: AGColumn[] = searchResults + .filter((item) => item.isChecked) + .map((it) => ({ + id: it.id, + name: it.name, + isVisible: true, + })); + + store.alertGroupStore.columns = [...store.alertGroupStore.columns, ...newColumns]; + + setIsModalOpen(false); + setTimeout(() => forceOpenToggletip(), 0); + setSearchResults([]); + + inputRef.current.value = ''; + } + function forceOpenToggletip() { document.getElementById('toggletip-button')?.click(); } From 9c14a894a7fd0fae3ecc45bfa2898b80e7fb666d Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Fri, 10 Nov 2023 13:48:21 +0200 Subject: [PATCH 18/67] readjust table on column change --- .../WithContextMenu/WithContextMenu.tsx | 4 +- .../src/models/alertgroup/alertgroup.ts | 18 +++---- .../src/models/alertgroup/alertgroup.types.ts | 6 +++ .../incidents/ColumnsSelectorWrapper.tsx | 8 +-- .../src/pages/incidents/Incidents.tsx | 50 +++++++++++-------- grafana-plugin/src/utils/consts.ts | 4 ++ grafana-plugin/src/utils/types.ts | 8 +++ 7 files changed, 62 insertions(+), 36 deletions(-) create mode 100644 grafana-plugin/src/utils/types.ts diff --git a/grafana-plugin/src/components/WithContextMenu/WithContextMenu.tsx b/grafana-plugin/src/components/WithContextMenu/WithContextMenu.tsx index 1b9617e02e..1c54785fb2 100644 --- a/grafana-plugin/src/components/WithContextMenu/WithContextMenu.tsx +++ b/grafana-plugin/src/components/WithContextMenu/WithContextMenu.tsx @@ -54,9 +54,7 @@ export const WithContextMenu: React.FC = ({ {isContextMenuOpen() && ( { - !forceIsOpen && setIsMenuOpen(false); - }} + onClose={() => !forceIsOpen && setIsMenuOpen(false)} x={menuPosition.x} y={menuPosition.y} renderMenuItems={() => renderMenuItems({ closeMenu: () => setIsMenuOpen(false) })} diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.ts b/grafana-plugin/src/models/alertgroup/alertgroup.ts index 2128893808..bf03ec1d79 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.ts @@ -12,7 +12,7 @@ import { SelectOption } from 'state/types'; import { openErrorNotification, refreshPageError, showApiError } from 'utils'; import LocationHelper from 'utils/LocationHelper'; -import { AGColumn, Alert, AlertAction, IncidentStatus } from './alertgroup.types'; +import { AGColumn, AGColumnType, Alert, AlertAction, IncidentStatus } from './alertgroup.types'; export class AlertGroupStore extends BaseStore { @observable.shallow @@ -77,14 +77,14 @@ export class AlertGroupStore extends BaseStore { @observable columns: AGColumn[] = [ - { id: 1, name: 'Status', isVisible: true }, - { id: 2, name: 'ID', isVisible: true }, - { id: 3, name: 'Summary', isVisible: true }, - { id: 4, name: 'Integration', isVisible: true }, - { id: 5, name: 'Users', isVisible: true }, - { id: 6, name: 'Team', isVisible: true }, - { id: 7, name: 'Cortex', isVisible: false }, - { id: 8, name: 'Created', isVisible: false }, + { id: 1, name: 'ID', isVisible: true, type: AGColumnType.DEFAULT }, + { id: 2, name: 'Status', isVisible: true, type: AGColumnType.DEFAULT }, + { id: 3, name: 'Alerts', isVisible: false, type: AGColumnType.DEFAULT }, + { id: 4, name: 'Source', isVisible: true, type: AGColumnType.DEFAULT }, + { id: 5, name: 'Created', isVisible: true, type: AGColumnType.DEFAULT }, + { id: 6, name: 'Team', isVisible: true, type: AGColumnType.DEFAULT }, + { id: 7, name: 'Users', isVisible: false, type: AGColumnType.DEFAULT }, + { id: 8, name: 'Bananas', isVisible: false, type: AGColumnType.LABEL }, ]; constructor(rootStore: RootStore) { diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.types.ts b/grafana-plugin/src/models/alertgroup/alertgroup.types.ts index a120fcb2e0..2ed923039f 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.types.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.types.ts @@ -92,6 +92,12 @@ export interface AGColumn { id: number | string; name: string; isVisible: boolean; + type?: AGColumnType; +} + +export enum AGColumnType { + DEFAULT = 'default', + LABEL = 'label', } interface RenderForWeb { diff --git a/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.tsx b/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.tsx index dafce9bc93..ea210f19b7 100644 --- a/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.tsx +++ b/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.tsx @@ -11,7 +11,7 @@ import { Label } from 'models/label/label.types'; import cn from 'classnames/bind'; -import styles from 'pages/incidents/ColumnsSelector.module.scss'; +import styles from 'pages/incidents/ColumnsSelectorWrapper.module.scss'; import { AGColumn } from 'models/alertgroup/alertgroup.types'; const cx = cn.bind(styles); @@ -104,7 +104,7 @@ const ColumnsModal: React.FC = ({ isModalOpen, labelKeys, set {labelKeys.length} items available. Type in to see suggestions )} - {searchResults.length && ( + {inputRef?.current?.value && searchResults.length && ( {searchResults.map((result) => ( @@ -144,7 +144,7 @@ const ColumnsModal: React.FC = ({ isModalOpen, labelKeys, set > Close - @@ -153,6 +153,8 @@ const ColumnsModal: React.FC = ({ isModalOpen, labelKeys, set ); function onAddNewColumns() { + // TODO: Backend Call instead! (once ready) + const newColumns: AGColumn[] = searchResults .filter((item) => item.isChecked) .map((it) => ({ diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index 7b650c50eb..62b8491e36 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -21,7 +21,14 @@ import { IncidentsFiltersType } from 'containers/IncidentsFilters/IncidentFilter import RemoteFilters from 'containers/RemoteFilters/RemoteFilters'; import TeamName from 'containers/TeamName/TeamName'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; -import { Alert, Alert as AlertType, AlertAction, IncidentStatus } from 'models/alertgroup/alertgroup.types'; +import { + Alert, + Alert as AlertType, + AlertAction, + IncidentStatus, + AGColumn, + AGColumnType, +} from 'models/alertgroup/alertgroup.types'; import { renderRelatedUsers } from 'pages/incident/Incident.helpers'; import { PageProps, WithStoreProps } from 'state/types'; import { withMobXProviderContext } from 'state/withStore'; @@ -33,6 +40,7 @@ import ColumnsSelectorWrapper from './ColumnsSelectorWrapper'; import styles from './Incidents.module.scss'; import { IncidentDropdown } from './parts/IncidentDropdown'; import { SilenceButtonCascader } from './parts/SilenceButtonCascader'; +import { TableColumn } from 'utils/types'; const cx = cn.bind(styles); @@ -615,59 +623,59 @@ class Incidents extends React.Component }); }; - getTableColumns(): Array<{ width: string; title: string; key: string; render }> { + getTableColumns(): TableColumn[] { const { store } = this.props; - return [ - { - width: '140px', - title: 'Status', - key: 'time', - render: this.renderStatus, - }, - { + const columnMapping: { [key: string]: TableColumn } = { + ID: { width: '10%', title: 'ID', key: 'id', render: this.renderId, }, - { - width: '35%', - title: 'Title', - key: 'title', - render: this.renderTitle, + Status: { + width: '140px', + title: 'Status', + key: 'time', + render: this.renderStatus, }, - { + Alerts: { width: '5%', title: 'Alerts', key: 'alerts', render: this.renderAlertsCounter, }, - { + Source: { width: '15%', title: 'Integration', key: 'source', render: this.renderSource, }, - { + Created: { width: '10%', title: 'Created', key: 'created', render: this.renderStartedAt, }, - { + Team: { width: '10%', title: 'Team', key: 'team', render: (item: AlertType) => this.renderTeam(item, store.grafanaTeamStore.items), }, - { + Users: { width: '15%', title: 'Users', key: 'users', render: renderRelatedUsers, }, - ]; + }; + + const mappedColumns: TableColumn[] = store.alertGroupStore.columns + .filter((col) => col.type === AGColumnType.DEFAULT && col.isVisible) + .map((column: AGColumn): TableColumn => columnMapping[column.name]); + + return mappedColumns; } getOnActionButtonClick = (incidentId: string, action: AlertAction): ((e: SyntheticEvent) => Promise) => { diff --git a/grafana-plugin/src/utils/consts.ts b/grafana-plugin/src/utils/consts.ts index 6195082dac..5488d028f4 100644 --- a/grafana-plugin/src/utils/consts.ts +++ b/grafana-plugin/src/utils/consts.ts @@ -54,3 +54,7 @@ export enum PAGE { } export const TEXT_ELLIPSIS_CLASS = 'overflow-child'; + +export enum AGColumn { + Status = 'status', +} \ No newline at end of file diff --git a/grafana-plugin/src/utils/types.ts b/grafana-plugin/src/utils/types.ts new file mode 100644 index 0000000000..ce16c6c6e8 --- /dev/null +++ b/grafana-plugin/src/utils/types.ts @@ -0,0 +1,8 @@ +import { RenderedCell } from 'rc-table/lib/interface'; + +export interface TableColumn { + width: any; + title: string; + key: string; + render: (value: any, record: any, index: number) => React.ReactNode | RenderedCell; +} From e3ac6b7a79ba485fec17b069266e794dad7663c9 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Fri, 10 Nov 2023 17:55:50 +0200 Subject: [PATCH 19/67] allow showing custom column (hardcoded for now) --- .../src/components/GTable/GTable.tsx | 2 +- .../src/models/alertgroup/alertgroup.ts | 1 + .../src/models/alertgroup/alertgroup.types.ts | 2 + .../ColumnsSelectorWrapper.module.scss | 17 +++ .../incidents/ColumnsSelectorWrapper.tsx | 109 ++++++++++++------ .../src/pages/incidents/Incidents.tsx | 25 +++- 6 files changed, 113 insertions(+), 43 deletions(-) diff --git a/grafana-plugin/src/components/GTable/GTable.tsx b/grafana-plugin/src/components/GTable/GTable.tsx index ac6283f4e6..5251e4e37f 100644 --- a/grafana-plugin/src/components/GTable/GTable.tsx +++ b/grafana-plugin/src/components/GTable/GTable.tsx @@ -115,7 +115,7 @@ const GTable = (props: Props = ({ isModalOpen, labelKeys, setIsModalOpen, inputRef }) => { const store = useStore(); const [searchResults, setSearchResults] = useState([]); @@ -92,44 +107,62 @@ const ColumnsModal: React.FC = ({ isModalOpen, labelKeys, set return ( setIsModalOpen(false)}> - - - {inputRef?.current?.value === '' && ( - {labelKeys.length} items available. Type in to see suggestions - )} - - {inputRef?.current?.value && searchResults.length && ( - - {searchResults.map((result) => ( - - { - setSearchResults((items) => { - return items.map((item) => { - const updatedItem: SearchResult = { ...item, isChecked: !item.isChecked }; - return item.id === result.id ? updatedItem : item; - }); - }); - }} - /> - - {result.name} - - ))} +
    + + + + {inputRef?.current?.value === '' && ( + {labelKeys.length} items available. Type in to see suggestions + )} + + {inputRef?.current?.value && searchResults.length && ( + + {searchResults.map((result) => ( +
    + { + setSearchResults((items) => { + return items.map((item) => { + const updatedItem: SearchResult = { ...item, isChecked: !item.isChecked }; + return item.id === result.id ? updatedItem : item; + }); + }); + }} + /> + + {result.name} + +
    + + {resultValues[KEY].map((value) => ( + + + {KEY} + : + {value} + + + ))} + +
    +
    + ))} +
    + )} + + {inputRef?.current?.value && searchResults.length === 0 && ( + 0 results for your search. + )}
    - )} - - {inputRef?.current?.value && searchResults.length === 0 && ( - 0 results for your search. - )} +
    ); - function onReset() {} + function onReset() { + alertGroupStore.columns = [...alertGroupStore.temporaryColumns]; + alertGroupStore.temporaryColumns = []; + } function onItemChange(id: string | number) { + const checkedItems = alertGroupStore.columns.filter((col) => col.isVisible); + if (checkedItems.length === 1 && checkedItems[0].id === id) { + openErrorNotification('At least one column should be selected'); + return; + } + alertGroupStore.columns = alertGroupStore.columns.map((item): AGColumn => { let newItem: AGColumn = { ...item, isVisible: !item.isVisible }; return item.id === id ? newItem : item; diff --git a/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.module.scss b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss similarity index 82% rename from grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.module.scss rename to grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss index ee4cb04e7b..64e45b5e1d 100644 --- a/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.module.scss +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss @@ -2,10 +2,6 @@ margin-bottom: 16px; } -.content-right { - margin-left: auto; -} - .field-row { width: 100%; display: flex; diff --git a/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx similarity index 81% rename from grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.tsx rename to grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx index 7cf8ac292e..9afa7107da 100644 --- a/grafana-plugin/src/pages/incidents/ColumnsSelectorWrapper.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx @@ -1,26 +1,16 @@ import React, { useEffect, useRef, useState } from 'react'; -import { - Button, - Checkbox, - HorizontalGroup, - Icon, - Input, - InlineLabel, - Modal, - Toggletip, - VerticalGroup, -} from '@grafana/ui'; +import { Button, Checkbox, HorizontalGroup, Icon, Input, Modal, Toggletip, VerticalGroup } from '@grafana/ui'; import cn from 'classnames/bind'; import Text from 'components/Text/Text'; import { AGColumn } from 'models/alertgroup/alertgroup.types'; import { Label } from 'models/label/label.types'; -import styles from 'pages/incidents/ColumnsSelectorWrapper.module.scss'; +import styles from 'containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss'; import { useStore } from 'state/useStore'; import { useDebouncedCallback } from 'utils/hooks'; -import { ColumnsSelector } from './ColumnsSelector'; +import { ColumnsSelector } from 'containers/ColumnsSelector/ColumnsSelector'; const cx = cn.bind(styles); @@ -92,18 +82,13 @@ interface SearchResult extends Label { isChecked: boolean; } -const KEY = 'test'; -const resultValues = { - [KEY]: ['eu-stage', 'us-central-prod'], -}; - const ColumnsModal: React.FC = ({ isModalOpen, labelKeys, setIsModalOpen, inputRef }) => { const store = useStore(); const [searchResults, setSearchResults] = useState([]); const debouncedOnInputChange = useDebouncedCallback(onInputChange, DEBOUNCE_MS); return ( - setIsModalOpen(false)}> + setIsModalOpen(false)}>
    @@ -121,8 +106,8 @@ const ColumnsModal: React.FC = ({ isModalOpen, labelKeys, set {inputRef?.current?.value && searchResults.length && ( - {searchResults.map((result) => ( -
    + {searchResults.map((result, index) => ( +
    = ({ isModalOpen, labelKeys, set /> {result.name} - -
    - - {resultValues[KEY].map((value) => ( - - - {KEY} - : - {value} - - - ))} - -
    ))} diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.ts b/grafana-plugin/src/models/alertgroup/alertgroup.ts index 9f17b22b87..fa30d63835 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.ts @@ -91,6 +91,9 @@ export class AlertGroupStore extends BaseStore { { id: 12, name: 'game', isVisible: false, type: AGColumnType.LABEL }, ]; + @observable + temporaryColumns: AGColumn[] = []; + constructor(rootStore: RootStore) { super(rootStore); diff --git a/grafana-plugin/src/pages/incidents/Incidents.module.scss b/grafana-plugin/src/pages/incidents/Incidents.module.scss index 09c936319c..5b1062ff72 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.module.scss +++ b/grafana-plugin/src/pages/incidents/Incidents.module.scss @@ -12,7 +12,11 @@ } .fields-dropdown { + gap: 8px; + display: flex; margin-left: auto; + align-items: center; + padding-left: 4px; } .above-incidents-table { diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index 8b11c41f42..627eecf8a0 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -18,6 +18,7 @@ import Text from 'components/Text/Text'; import TextEllipsisTooltip from 'components/TextEllipsisTooltip/TextEllipsisTooltip'; import Tutorial from 'components/Tutorial/Tutorial'; import { TutorialStep } from 'components/Tutorial/Tutorial.types'; +import ColumnsSelectorWrapper from 'containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper'; import { IncidentsFiltersType } from 'containers/IncidentsFilters/IncidentFilters.types'; import RemoteFilters from 'containers/RemoteFilters/RemoteFilters'; import TeamName from 'containers/TeamName/TeamName'; @@ -31,6 +32,7 @@ import { AGColumnType, } from 'models/alertgroup/alertgroup.types'; import { renderRelatedUsers } from 'pages/incident/Incident.helpers'; +import { AppFeature } from 'state/features'; import { PageProps, WithStoreProps } from 'state/types'; import { withMobXProviderContext } from 'state/withStore'; import LocationHelper from 'utils/LocationHelper'; @@ -38,7 +40,6 @@ import { UserActions } from 'utils/authorization'; import { PAGE, PLUGIN_ROOT, TEXT_ELLIPSIS_CLASS } from 'utils/consts'; import { TableColumn } from 'utils/types'; -import ColumnsSelectorWrapper from './ColumnsSelectorWrapper'; import styles from './Incidents.module.scss'; import { IncidentDropdown } from './parts/IncidentDropdown'; import { SilenceButtonCascader } from './parts/SilenceButtonCascader'; @@ -435,23 +436,23 @@ class Incidents extends React.Component
    - -
    -
    + {hasInvalidatedAlert && ( + + Results out of date + + + )} - {hasInvalidatedAlert && ( -
    - Results out of date - + {store.hasFeature(AppFeature.Labels) && }
    - )} +
    ); }; From bf452757425d8406424647a99d4a4bfbb35b4c7b Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Tue, 14 Nov 2023 09:07:47 +0200 Subject: [PATCH 24/67] horizontal scrolling, fixed resetting data --- .../src/containers/ColumnsSelector/ColumnsSelector.tsx | 1 - .../ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx | 8 ++++++++ grafana-plugin/src/pages/incidents/Incidents.tsx | 3 ++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx index fffe691b5b..eb69bf3e31 100644 --- a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx +++ b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx @@ -171,7 +171,6 @@ export const ColumnsSelector: React.FC = observer(({ onMod function onReset() { alertGroupStore.columns = [...alertGroupStore.temporaryColumns]; - alertGroupStore.temporaryColumns = []; } function onItemChange(id: string | number) { diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx index 9afa7107da..12aa0bbec3 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx @@ -50,6 +50,7 @@ const ColumnsSelectorWrapper: React.FC = () => { placement={'bottom-end'} show={true} closeButton={false} + onClose={onToggletipClose} > {renderToggletipButton()} @@ -69,6 +70,13 @@ const ColumnsSelectorWrapper: React.FC = () => { ); } + + function onToggletipClose() { + const { alertGroupStore } = store; + + // reset temporary cached columns + alertGroupStore.temporaryColumns = [...alertGroupStore.columns]; + } }; interface ColumnsModalProps { diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index 627eecf8a0..2dfec1deac 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -500,6 +500,7 @@ class Incidents extends React.Component data={results} columns={this.getTableColumns()} tableLayout="auto" + scroll={{ x: true }} /> {this.shouldShowPagination() && (
    @@ -521,7 +522,7 @@ class Incidents extends React.Component renderId(record: AlertType) { return ( - + #{record.inside_organization_number} From 855927da6c94077e897512564f4b8d25e3953aa4 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Tue, 14 Nov 2023 09:09:22 +0200 Subject: [PATCH 25/67] lint --- .../src/containers/ColumnsSelector/ColumnsSelector.tsx | 2 +- .../ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx index eb69bf3e31..37d534e8f8 100644 --- a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx +++ b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx @@ -24,8 +24,8 @@ import { observer } from 'mobx-react'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; import Text from 'components/Text/Text'; -import { AGColumn, AGColumnType } from 'models/alertgroup/alertgroup.types'; import styles from 'containers/ColumnsSelector/ColumnsSelector.module.scss'; +import { AGColumn, AGColumnType } from 'models/alertgroup/alertgroup.types'; import { useStore } from 'state/useStore'; import { openErrorNotification } from 'utils'; diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx index 12aa0bbec3..4f2d385e8c 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx @@ -4,14 +4,13 @@ import { Button, Checkbox, HorizontalGroup, Icon, Input, Modal, Toggletip, Verti import cn from 'classnames/bind'; import Text from 'components/Text/Text'; +import { ColumnsSelector } from 'containers/ColumnsSelector/ColumnsSelector'; +import styles from 'containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss'; import { AGColumn } from 'models/alertgroup/alertgroup.types'; import { Label } from 'models/label/label.types'; -import styles from 'containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss'; import { useStore } from 'state/useStore'; import { useDebouncedCallback } from 'utils/hooks'; -import { ColumnsSelector } from 'containers/ColumnsSelector/ColumnsSelector'; - const cx = cn.bind(styles); interface ColumnsSelectorWrapperProps {} From 75b8f3682b2a86375afe0fed4396720243558a74 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 16 Nov 2023 12:29:39 +0100 Subject: [PATCH 26/67] Add endpoint for alert group columns selector --- .../serializers/alert_group_table_settings.py | 48 +++++++++++++++++++ engine/apps/api/urls.py | 12 +++++ .../api/views/alert_group_table_settings.py | 48 +++++++++++++++++++ engine/apps/user_management/constants.py | 31 ++++++++++++ .../migrations/0018_auto_20231115_1206.py | 24 ++++++++++ .../user_management/models/organization.py | 10 +++- engine/apps/user_management/models/user.py | 7 +++ engine/apps/user_management/utils.py | 32 +++++++++++++ 8 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 engine/apps/api/serializers/alert_group_table_settings.py create mode 100644 engine/apps/api/views/alert_group_table_settings.py create mode 100644 engine/apps/user_management/constants.py create mode 100644 engine/apps/user_management/migrations/0018_auto_20231115_1206.py create mode 100644 engine/apps/user_management/utils.py diff --git a/engine/apps/api/serializers/alert_group_table_settings.py b/engine/apps/api/serializers/alert_group_table_settings.py new file mode 100644 index 0000000000..ad499ad4af --- /dev/null +++ b/engine/apps/api/serializers/alert_group_table_settings.py @@ -0,0 +1,48 @@ +from rest_framework import serializers +from rest_framework.exceptions import ValidationError + +from apps.user_management.constants import AlertGroupTableColumnChoices, AlertGroupTableColumnTypeChoices + + +class ColumnIdField(serializers.Field): + def to_representation(self, value): + return value + + def to_internal_value(self, data): + if isinstance(data, int) or isinstance(data, str): + return data + else: + raise ValidationError("Invalid column id format") + + +class AlertGroupTableColumnSerializer(serializers.Serializer): + name = serializers.CharField(max_length=200) + id = ColumnIdField() + type = serializers.ChoiceField(choices=AlertGroupTableColumnTypeChoices.choices) + + def validate(self, data): + # todo: check if id for default is int and for label is str + # todo: check if every column exists in organization columns list if not is_org_settings + return data + + def _validate_id(self, column_id, column_type): + # todo + if ( + column_type == AlertGroupTableColumnTypeChoices.DEFAULT.value + and column_id not in AlertGroupTableColumnChoices.values + ): + raise ValidationError("Invalid column id format") + + +class AlertGroupTableColumnsListSerializer(serializers.Serializer): + visible = AlertGroupTableColumnSerializer(many=True) + hidden = AlertGroupTableColumnSerializer(many=True) + + def is_org_settings(self): # todo + return self.context.get("is_org_settings") is True + + def validate(self, data): + # todo: check columns list if not is_org_settings (should be the same) + # todo: check if all default columns are in the list if is_org_settings + # todo: minimum one columns should be visible + return data diff --git a/engine/apps/api/urls.py b/engine/apps/api/urls.py index c8c79a1e67..0091d987ba 100644 --- a/engine/apps/api/urls.py +++ b/engine/apps/api/urls.py @@ -4,6 +4,7 @@ from .views import UserNotificationPolicyView, auth from .views.alert_group import AlertGroupView +from .views.alert_group_table_settings import AlertGroupTableColumnsViewSet from .views.alert_receive_channel import AlertReceiveChannelView from .views.alert_receive_channel_template import AlertReceiveChannelTemplateView from .views.alerts import AlertDetailView @@ -143,3 +144,14 @@ name="alert_group_labels-get_key", ), ] + +# Alert group table settings +urlpatterns += [ + re_path( + r"^alertgroup_table_settings/?$", + AlertGroupTableColumnsViewSet.as_view( + {"get": "get_columns", "put": "update_columns_settings", "post": "update_columns_list"} + ), + name="alert_group_table-columns_settings", + ) +] diff --git a/engine/apps/api/views/alert_group_table_settings.py b/engine/apps/api/views/alert_group_table_settings.py new file mode 100644 index 0000000000..c08ca994ba --- /dev/null +++ b/engine/apps/api/views/alert_group_table_settings.py @@ -0,0 +1,48 @@ +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.viewsets import ViewSet + +from apps.api.permissions import RBACPermission +from apps.api.serializers.alert_group_table_settings import AlertGroupTableColumnsListSerializer +from apps.auth_token.auth import PluginAuthentication +from apps.user_management.utils import alert_group_table_user_settings + + +class AlertGroupTableColumnsViewSet(ViewSet): + authentication_classes = (PluginAuthentication,) + permission_classes = (IsAuthenticated, RBACPermission) + + rbac_permissions = { + "get_columns": [RBACPermission.Permissions.ALERT_GROUPS_READ], + "update_columns_settings": [RBACPermission.Permissions.ALERT_GROUPS_READ], + "update_columns_list": [RBACPermission.Permissions.OTHER_SETTINGS_WRITE], + } + + def get_columns(self, request): + user = request.user + return Response(alert_group_table_user_settings(user)) + + def update_columns_list(self, request): + """add/remove columns for organization""" + user = request.user + organization = request.auth.organization + serializer = AlertGroupTableColumnsListSerializer( + data=request.data, context={"request": request, "is_org_settings": True} + ) + serializer.is_valid(raise_exception=True) + columns = [column for column in request.data.get("visible", [])] + [ + column for column in request.data.get("hidden", []) + ] + organization.update_alert_group_table_columns(columns) + return Response(alert_group_table_user_settings(user)) + + def update_columns_settings(self, request): + """select/hide/change order for user""" + user = request.user + serializer = AlertGroupTableColumnsListSerializer( + data=request.data, context={"request": request, "is_org_settings": False} + ) + serializer.is_valid(raise_exception=True) + columns = [column for column in request.data.get("visible", [])] + user.update_alert_group_table_columns_settings(columns) + return Response(alert_group_table_user_settings(user)) diff --git a/engine/apps/user_management/constants.py b/engine/apps/user_management/constants.py new file mode 100644 index 0000000000..12539dae17 --- /dev/null +++ b/engine/apps/user_management/constants.py @@ -0,0 +1,31 @@ +import typing + +from django.db.models import IntegerChoices, TextChoices + + +class AlertGroupTableColumnChoices(IntegerChoices): + STATUS = 1, "Status" + ID = 2, "ID" + TITLE = 3, "Title" + ALERTS = 4, "Alerts" + INTEGRATION = 5, "Integration" + CREATED = 6, "Created" + LABELS = 7, "Labels" + TEAM = 8, "Team" + USERS = 9, "Users" + + +class AlertGroupTableColumnTypeChoices(TextChoices): + DEFAULT = "default" + LABEL = "label" + + +class AlertGroupTableColumn(typing.TypedDict): + id: str | int + name: str + type: str + + +class AlertGroupTableColumns(typing.TypedDict): + visible: typing.List[AlertGroupTableColumn] + hidden: typing.List[AlertGroupTableColumn] diff --git a/engine/apps/user_management/migrations/0018_auto_20231115_1206.py b/engine/apps/user_management/migrations/0018_auto_20231115_1206.py new file mode 100644 index 0000000000..4d6b29b77b --- /dev/null +++ b/engine/apps/user_management/migrations/0018_auto_20231115_1206.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.20 on 2023-11-15 12:06 + +import apps.user_management.utils +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user_management', '0017_alter_organization_maintenance_author'), + ] + + operations = [ + migrations.AddField( + model_name='organization', + name='alert_group_table_columns', + field=models.JSONField(default=apps.user_management.utils.default_columns), + ), + migrations.AddField( + model_name='user', + name='alert_groups_table_selected_columns', + field=models.JSONField(default=None, null=True), + ), + ] diff --git a/engine/apps/user_management/models/organization.py b/engine/apps/user_management/models/organization.py index 70fcf8e002..30e3efebde 100644 --- a/engine/apps/user_management/models/organization.py +++ b/engine/apps/user_management/models/organization.py @@ -6,12 +6,13 @@ from django.conf import settings from django.core.validators import MinLengthValidator from django.db import models -from django.db.models import Count, Q +from django.db.models import Count, JSONField, Q from django.utils import timezone from mirage import fields as mirage_fields from apps.alerts.models import MaintainableObject from apps.user_management.subscription_strategy import FreePublicBetaSubscriptionStrategy +from apps.user_management.utils import default_columns from common.insight_log import ChatOpsEvent, ChatOpsTypePlug, write_chatops_insight_log from common.oncall_gateway import create_oncall_connector, delete_oncall_connector, delete_slack_connector from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length @@ -248,6 +249,8 @@ def _get_subscription_strategy(self): is_rbac_permissions_enabled = models.BooleanField(default=False) is_grafana_incident_enabled = models.BooleanField(default=False) + alert_group_table_columns = JSONField(default=default_columns) + class Meta: unique_together = ("stack_id", "org_id") @@ -283,6 +286,11 @@ def sms_left(self, user): def emails_left(self, user): return self.subscription_strategy.emails_left(user) + def update_alert_group_table_columns(self, columns: list): + if columns != self.alert_group_table_columns: + self.alert_group_table_columns = columns + self.save(update_fields=["alert_group_table_columns"]) + def set_general_log_channel(self, channel_id, channel_name, user): if self.general_log_channel_id != channel_id: old_general_log_channel_id = self.slack_team_identity.cached_channels.filter( diff --git a/engine/apps/user_management/models/user.py b/engine/apps/user_management/models/user.py index d6dbbd710d..418c33f068 100644 --- a/engine/apps/user_management/models/user.py +++ b/engine/apps/user_management/models/user.py @@ -236,6 +236,8 @@ class Meta: is_active = models.BooleanField(null=True, default=True) permissions = models.JSONField(null=False, default=list) + alert_groups_table_selected_columns = models.JSONField(default=None, null=True) + def __str__(self): return f"{self.pk}: {self.username}" @@ -449,6 +451,11 @@ def important_notification_policies_defaults(self): ), ) + def update_alert_group_table_columns_settings(self, columns: list): + if self.alert_groups_table_selected_columns != columns: + self.alert_groups_table_selected_columns = columns + self.save(update_fields=["alert_groups_table_selected_columns"]) + # TODO: check whether this signal can be moved to save method of the model @receiver(post_save, sender=User) diff --git a/engine/apps/user_management/utils.py b/engine/apps/user_management/utils.py new file mode 100644 index 0000000000..23d8a129ba --- /dev/null +++ b/engine/apps/user_management/utils.py @@ -0,0 +1,32 @@ +import typing + +from apps.user_management.constants import ( + AlertGroupTableColumn, + AlertGroupTableColumnChoices, + AlertGroupTableColumns, + AlertGroupTableColumnTypeChoices, +) + + +def default_columns(): + columns = [ + {"name": column.label, "id": column.value, "type": AlertGroupTableColumnTypeChoices.DEFAULT.value} + for column in AlertGroupTableColumnChoices + ] + return columns + + +def alert_group_table_user_settings(user) -> AlertGroupTableColumns: + organization_columns = user.organization.alert_group_table_columns + visible_columns: typing.List[AlertGroupTableColumn] + if user.alert_groups_table_selected_columns: + visible_columns = [ + column for column in user.alert_groups_table_selected_columns if column in organization_columns + ] + else: + visible_columns = default_columns() + user.update_alert_group_table_columns_settings(visible_columns) + hidden_columns: typing.List[AlertGroupTableColumn] = [ + column for column in organization_columns if column not in visible_columns + ] + return {"visible": visible_columns, "hidden": hidden_columns} From fced3ff2266b65f94d919f0b46f5490b8e5635c9 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Thu, 16 Nov 2023 16:52:28 +0200 Subject: [PATCH 27/67] changes wip --- grafana-plugin/src/assets/style/utils.css | 4 ++ .../ColumnsSelectorWrapper.tsx | 54 +++++++++++++------ .../src/models/alertgroup/alertgroup.ts | 51 +++++++++--------- .../src/models/loader/action-keys.ts | 3 ++ grafana-plugin/src/models/loader/loader.ts | 20 +++++++ .../src/pages/incidents/Incidents.tsx | 2 +- grafana-plugin/src/utils/decorators.ts | 15 ++++++ 7 files changed, 107 insertions(+), 42 deletions(-) create mode 100644 grafana-plugin/src/models/loader/action-keys.ts create mode 100644 grafana-plugin/src/models/loader/loader.ts create mode 100644 grafana-plugin/src/utils/decorators.ts diff --git a/grafana-plugin/src/assets/style/utils.css b/grafana-plugin/src/assets/style/utils.css index 7b02d032fd..801bb542cf 100644 --- a/grafana-plugin/src/assets/style/utils.css +++ b/grafana-plugin/src/assets/style/utils.css @@ -117,6 +117,10 @@ * Icons */ + .loader { + margin-bottom: 0 !important; + } + .icon-exclamation { color: var(--error-text-color); } diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx index 4f2d385e8c..d9e1ad9053 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx @@ -1,15 +1,28 @@ import React, { useEffect, useRef, useState } from 'react'; -import { Button, Checkbox, HorizontalGroup, Icon, Input, Modal, Toggletip, VerticalGroup } from '@grafana/ui'; +import { + Button, + Checkbox, + HorizontalGroup, + Icon, + Input, + LoadingPlaceholder, + Modal, + Toggletip, + VerticalGroup, +} from '@grafana/ui'; import cn from 'classnames/bind'; import Text from 'components/Text/Text'; import { ColumnsSelector } from 'containers/ColumnsSelector/ColumnsSelector'; import styles from 'containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss'; -import { AGColumn } from 'models/alertgroup/alertgroup.types'; +import { AGColumn, AGColumnType } from 'models/alertgroup/alertgroup.types'; import { Label } from 'models/label/label.types'; import { useStore } from 'state/useStore'; import { useDebouncedCallback } from 'utils/hooks'; +import LoaderStore from 'models/loader/loader'; +import { ActionKey } from 'models/loader/action-keys'; +import { observer } from 'mobx-react'; const cx = cn.bind(styles); @@ -89,11 +102,13 @@ interface SearchResult extends Label { isChecked: boolean; } -const ColumnsModal: React.FC = ({ isModalOpen, labelKeys, setIsModalOpen, inputRef }) => { +const ColumnsModal: React.FC = observer(({ isModalOpen, labelKeys, setIsModalOpen, inputRef }) => { const store = useStore(); const [searchResults, setSearchResults] = useState([]); const debouncedOnInputChange = useDebouncedCallback(onInputChange, DEBOUNCE_MS); + const isLoading = LoaderStore.isLoading(ActionKey.IS_ADDING_NEW_COLUMN_TO_ALERT_GROUP); + return ( setIsModalOpen(false)}> @@ -108,7 +123,7 @@ const ColumnsModal: React.FC = ({ isModalOpen, labelKeys, set /> {inputRef?.current?.value === '' && ( - {labelKeys.length} items available. Type in to see suggestions + {labelKeys.length} items available. Type to see suggestions )} {inputRef?.current?.value && searchResults.length && ( @@ -153,26 +168,33 @@ const ColumnsModal: React.FC = ({ isModalOpen, labelKeys, set > Close - ); - function onAddNewColumns() { - // TODO: Backend Call instead! (once ready) - + async function onAddNewColumns() { const newColumns: AGColumn[] = searchResults .filter((item) => item.isChecked) - .map((it) => ({ - id: it.id, - name: it.name, - isVisible: true, - })); + .map( + (it): AGColumn => ({ + id: it.id, + name: it.name, + isVisible: false, + type: AGColumnType.LABEL, + }) + ); + + const allColumns = [...store.alertGroupStore.columns, ...newColumns]; - store.alertGroupStore.columns = [...store.alertGroupStore.columns, ...newColumns]; + await store.alertGroupStore.updateTableSettings(allColumns, false); setIsModalOpen(false); setTimeout(() => forceOpenToggletip(), 0); @@ -191,6 +213,6 @@ const ColumnsModal: React.FC = ({ isModalOpen, labelKeys, set labelKeys.filter((pair) => pair.name.indexOf(search) > -1).map((pair) => ({ ...pair, isChecked: false })) ); } -}; +}); export default ColumnsSelectorWrapper; diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.ts b/grafana-plugin/src/models/alertgroup/alertgroup.ts index fa30d63835..2b1adc5240 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.ts @@ -12,7 +12,9 @@ import { SelectOption } from 'state/types'; import { openErrorNotification, refreshPageError, showApiError } from 'utils'; import LocationHelper from 'utils/LocationHelper'; -import { AGColumn, AGColumnType, Alert, AlertAction, IncidentStatus } from './alertgroup.types'; +import { AGColumn, Alert, AlertAction, IncidentStatus } from './alertgroup.types'; +import { AutoLoadingState } from 'utils/decorators'; +import { ActionKey } from 'models/loader/action-keys'; export class AlertGroupStore extends BaseStore { @observable.shallow @@ -76,20 +78,7 @@ export class AlertGroupStore extends BaseStore { liveUpdatesPaused = false; @observable - columns: AGColumn[] = [ - { id: 1, name: 'ID', isVisible: true, type: AGColumnType.DEFAULT }, - { id: 2, name: 'Status', isVisible: true, type: AGColumnType.DEFAULT }, - { id: 3, name: 'Alerts', isVisible: true, type: AGColumnType.DEFAULT }, - { id: 4, name: 'Source', isVisible: true, type: AGColumnType.DEFAULT }, - { id: 5, name: 'Created', isVisible: true, type: AGColumnType.DEFAULT }, - { id: 6, name: 'Team', isVisible: true, type: AGColumnType.DEFAULT }, - { id: 7, name: 'Users', isVisible: false, type: AGColumnType.DEFAULT }, - { id: 8, name: 'Title', isVisible: false, type: AGColumnType.DEFAULT }, - { id: 9, name: 'a', isVisible: false, type: AGColumnType.LABEL }, - { id: 10, name: 'color', isVisible: false, type: AGColumnType.LABEL }, - { id: 11, name: 'country', isVisible: false, type: AGColumnType.LABEL }, - { id: 12, name: 'game', isVisible: false, type: AGColumnType.LABEL }, - ]; + columns: AGColumn[] = []; @observable temporaryColumns: AGColumn[] = []; @@ -458,17 +447,29 @@ export class AlertGroupStore extends BaseStore { }).catch(this.onApiError); } + // api/internal/v1/alertgroup_table_settings + + @action + public async fetchTableSettings(): Promise { + const tableSettings = await makeRequest('/alertgroup_table_settings', {}); + + const { hidden, visible } = tableSettings; + + this.columns = [ + ...visible.map((item: AGColumn): AGColumn => ({ ...item, isVisible: true })), + ...hidden.map((item: AGColumn): AGColumn => ({ ...item, isVisible: false })), + ]; + } + @action - public async fetchAllColumnKeys(): Promise { - const keys = await this.loadLabelsKeys(); - const results: AGColumn[] = keys.map((key) => ({ - id: key.id, - name: key.name, - isVisible: false, - type: AGColumnType.LABEL, - })); - - this.columns = [...this.columns] || [...results]; // TODO; change once backend is ready + @AutoLoadingState(ActionKey.IS_ADDING_NEW_COLUMN_TO_ALERT_GROUP) + public async updateTableSettings(columns: AGColumn[], isUserUpdate: boolean): Promise { + const method = isUserUpdate ? 'PUT' : 'POST'; + + await makeRequest('/alertgroup_table_settings', { + method, + data: [...columns], + }); } @action diff --git a/grafana-plugin/src/models/loader/action-keys.ts b/grafana-plugin/src/models/loader/action-keys.ts new file mode 100644 index 0000000000..dcadf10b82 --- /dev/null +++ b/grafana-plugin/src/models/loader/action-keys.ts @@ -0,0 +1,3 @@ +export enum ActionKey { + IS_ADDING_NEW_COLUMN_TO_ALERT_GROUP = 'IS_ADDING_NEW_COLUMN_TO_ALERT_GROUP', +} diff --git a/grafana-plugin/src/models/loader/loader.ts b/grafana-plugin/src/models/loader/loader.ts new file mode 100644 index 0000000000..fd554798ca --- /dev/null +++ b/grafana-plugin/src/models/loader/loader.ts @@ -0,0 +1,20 @@ +import { observable } from 'mobx'; + +interface LoadingResult { + [key: string]: boolean; +} + +class LoaderStore { + @observable + items: LoadingResult = {}; + + setLoadingAction(actionKey: string, isLoading: boolean) { + this.items[actionKey] = isLoading; + } + + isLoading(actionKey: string): boolean { + return !!this.items[actionKey]; + } +} + +export default new LoaderStore() as LoaderStore; diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index 2dfec1deac..fe92fa735a 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -103,7 +103,7 @@ class Incidents extends React.Component alertGroupStore.updateBulkActions(); alertGroupStore.updateSilenceOptions(); - alertGroupStore.fetchAllColumnKeys(); + alertGroupStore.fetchTableSettings(); } componentWillUnmount(): void { diff --git a/grafana-plugin/src/utils/decorators.ts b/grafana-plugin/src/utils/decorators.ts new file mode 100644 index 0000000000..6b4a1892a5 --- /dev/null +++ b/grafana-plugin/src/utils/decorators.ts @@ -0,0 +1,15 @@ +import LoaderStore from 'models/loader/loader'; + +export function AutoLoadingState(actionKey: string) { + return function (_target: object, _key: string, descriptor: PropertyDescriptor) { + const originalFunction = descriptor.value; + descriptor.value = async function (...args: any) { + LoaderStore.setLoadingAction(actionKey, true); + try { + await originalFunction.apply(this, args); + } finally { + LoaderStore.setLoadingAction(actionKey, false); + } + }; + }; +} From 317e972bfa0bb0e9c89dae3c75b39ed49487b20e Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 16 Nov 2023 17:04:51 +0100 Subject: [PATCH 28/67] Add validation for columns settings endpoint --- .../serializers/alert_group_table_settings.py | 41 +++++++++++++------ .../api/views/alert_group_table_settings.py | 11 +++-- engine/apps/user_management/constants.py | 2 +- engine/apps/user_management/utils.py | 4 +- 4 files changed, 38 insertions(+), 20 deletions(-) diff --git a/engine/apps/api/serializers/alert_group_table_settings.py b/engine/apps/api/serializers/alert_group_table_settings.py index ad499ad4af..c3ce1acda7 100644 --- a/engine/apps/api/serializers/alert_group_table_settings.py +++ b/engine/apps/api/serializers/alert_group_table_settings.py @@ -1,7 +1,7 @@ from rest_framework import serializers from rest_framework.exceptions import ValidationError -from apps.user_management.constants import AlertGroupTableColumnChoices, AlertGroupTableColumnTypeChoices +from apps.user_management.constants import AlertGroupTableColumnTypeChoices, AlertGroupTableDefaultColumnChoices class ColumnIdField(serializers.Field): @@ -21,15 +21,13 @@ class AlertGroupTableColumnSerializer(serializers.Serializer): type = serializers.ChoiceField(choices=AlertGroupTableColumnTypeChoices.choices) def validate(self, data): - # todo: check if id for default is int and for label is str - # todo: check if every column exists in organization columns list if not is_org_settings + self._validate_id(data) return data - def _validate_id(self, column_id, column_type): - # todo + def _validate_id(self, data): if ( - column_type == AlertGroupTableColumnTypeChoices.DEFAULT.value - and column_id not in AlertGroupTableColumnChoices.values + data["type"] == AlertGroupTableColumnTypeChoices.DEFAULT.value + and data["id"] not in AlertGroupTableDefaultColumnChoices.values ): raise ValidationError("Invalid column id format") @@ -38,11 +36,28 @@ class AlertGroupTableColumnsListSerializer(serializers.Serializer): visible = AlertGroupTableColumnSerializer(many=True) hidden = AlertGroupTableColumnSerializer(many=True) - def is_org_settings(self): # todo - return self.context.get("is_org_settings") is True - def validate(self, data): - # todo: check columns list if not is_org_settings (should be the same) - # todo: check if all default columns are in the list if is_org_settings - # todo: minimum one columns should be visible + """ + Validate data regarding if it updates alert group table columns settings for organization or for user: + + `is_org_settings=True` means that organization alert group table columns list should be updated. + Validate that all default columns are in the list. + + `is_org_settings=False` means that list of visible columns for user should be updated. + Validate that all columns exist in organization alert group table columns list and at least one column is + selected as visible. + """ + is_org_settings = self.context.get("is_org_settings") is True + organization = self.context["request"].auth.organization + columns_list = data["visible"] + data["hidden"] + request_columns_ids = [column["id"] for column in columns_list] + if is_org_settings: + if not set(request_columns_ids) >= set(AlertGroupTableDefaultColumnChoices.values): + raise ValidationError("Default column cannot be removed") + else: + if len(data["visible"]) == 0: + raise ValidationError("At least one column should be selected as visible") + organization_columns_ids = [column["id"] for column in organization.alert_group_table_columns] + if set(organization_columns_ids) != set(request_columns_ids): + raise ValidationError("Invalid settings") return data diff --git a/engine/apps/api/views/alert_group_table_settings.py b/engine/apps/api/views/alert_group_table_settings.py index c08ca994ba..f3b8c15899 100644 --- a/engine/apps/api/views/alert_group_table_settings.py +++ b/engine/apps/api/views/alert_group_table_settings.py @@ -1,3 +1,5 @@ +import typing + from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.viewsets import ViewSet @@ -5,6 +7,7 @@ from apps.api.permissions import RBACPermission from apps.api.serializers.alert_group_table_settings import AlertGroupTableColumnsListSerializer from apps.auth_token.auth import PluginAuthentication +from apps.user_management.constants import AlertGroupTableColumn from apps.user_management.utils import alert_group_table_user_settings @@ -30,9 +33,9 @@ def update_columns_list(self, request): data=request.data, context={"request": request, "is_org_settings": True} ) serializer.is_valid(raise_exception=True) - columns = [column for column in request.data.get("visible", [])] + [ - column for column in request.data.get("hidden", []) - ] + columns: typing.List[AlertGroupTableColumn] = serializer.validated_data.get( + "visible", [] + ) + serializer.validated_data.get("hidden", []) organization.update_alert_group_table_columns(columns) return Response(alert_group_table_user_settings(user)) @@ -43,6 +46,6 @@ def update_columns_settings(self, request): data=request.data, context={"request": request, "is_org_settings": False} ) serializer.is_valid(raise_exception=True) - columns = [column for column in request.data.get("visible", [])] + columns: typing.List[AlertGroupTableColumn] = serializer.validated_data.get("visible", []) user.update_alert_group_table_columns_settings(columns) return Response(alert_group_table_user_settings(user)) diff --git a/engine/apps/user_management/constants.py b/engine/apps/user_management/constants.py index 12539dae17..9b09356c61 100644 --- a/engine/apps/user_management/constants.py +++ b/engine/apps/user_management/constants.py @@ -3,7 +3,7 @@ from django.db.models import IntegerChoices, TextChoices -class AlertGroupTableColumnChoices(IntegerChoices): +class AlertGroupTableDefaultColumnChoices(IntegerChoices): STATUS = 1, "Status" ID = 2, "ID" TITLE = 3, "Title" diff --git a/engine/apps/user_management/utils.py b/engine/apps/user_management/utils.py index 23d8a129ba..e98a24c711 100644 --- a/engine/apps/user_management/utils.py +++ b/engine/apps/user_management/utils.py @@ -2,16 +2,16 @@ from apps.user_management.constants import ( AlertGroupTableColumn, - AlertGroupTableColumnChoices, AlertGroupTableColumns, AlertGroupTableColumnTypeChoices, + AlertGroupTableDefaultColumnChoices, ) def default_columns(): columns = [ {"name": column.label, "id": column.value, "type": AlertGroupTableColumnTypeChoices.DEFAULT.value} - for column in AlertGroupTableColumnChoices + for column in AlertGroupTableDefaultColumnChoices ] return columns From 3c99bb4f244ae6cb7ee9bd7a565c2948d2b0b23d Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Fri, 17 Nov 2023 16:30:29 +0200 Subject: [PATCH 29/67] connected endpoints --- .../ColumnsSelector/ColumnsSelector.tsx | 65 +++++--- .../ColumnsSelectorWrapper.tsx | 141 ++++++++++++------ .../src/models/alertgroup/alertgroup.ts | 9 +- .../src/pages/incidents/Incidents.tsx | 4 +- .../src/pages/integration/Integration.tsx | 2 +- 5 files changed, 153 insertions(+), 68 deletions(-) diff --git a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx index 37d534e8f8..1744543f36 100644 --- a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx +++ b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx @@ -19,7 +19,7 @@ import { import { CSS } from '@dnd-kit/utilities'; import { Button, Checkbox, Icon, IconButton, Tooltip } from '@grafana/ui'; import cn from 'classnames/bind'; -import { cloneDeep, isEqual, noop } from 'lodash-es'; +import { cloneDeep, isEqual } from 'lodash-es'; import { observer } from 'mobx-react'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; @@ -28,6 +28,8 @@ import styles from 'containers/ColumnsSelector/ColumnsSelector.module.scss'; import { AGColumn, AGColumnType } from 'models/alertgroup/alertgroup.types'; import { useStore } from 'state/useStore'; import { openErrorNotification } from 'utils'; +import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; +import { UserActions } from 'utils/authorization'; const cx = cn.bind(styles); const TRANSITION_MS = 500; @@ -35,9 +37,10 @@ const TRANSITION_MS = 500; interface ColumnRowProps { column: AGColumn; onItemChange: (id: number | string) => void; + onColumnRemoval: (column: AGColumn) => void; } -const ColumnRow: React.FC = ({ column, onItemChange }) => { +const ColumnRow: React.FC = ({ column, onItemChange, onColumnRemoval }) => { const dnd = useSortable({ id: column.id }); const { attributes, listeners, setNodeRef, transform, transition } = dnd; @@ -67,14 +70,16 @@ const ColumnRow: React.FC = ({ column, onItemChange }) => { {...attributes} {...listeners} /> - ) : ( - - )} + ) : column.type === AGColumnType.LABEL ? ( + + onColumnRemoval(column)} + /> + + ) : undefined}
    = ({ column, onItemChange }) => { }; interface ColumnsSelectorProps { - onModalOpen(): void; + onColumnAddModalOpen(): void; + onConfirmRemovalModalOpen(column: AGColumn): void; } -export const ColumnsSelector: React.FC = observer(({ onModalOpen }) => { +export const ColumnsSelector: React.FC = observer(({ onColumnAddModalOpen, onConfirmRemovalModalOpen }) => { const { alertGroupStore } = useStore(); const { columns: items, temporaryColumns } = alertGroupStore; @@ -132,7 +138,12 @@ export const ColumnsSelector: React.FC = observer(({ onMod {visibleColumns.map((column) => ( - + ))} @@ -150,7 +161,12 @@ export const ColumnsSelector: React.FC = observer(({ onMod {hiddenColumns.map((column) => ( - + ))} @@ -162,9 +178,11 @@ export const ColumnsSelector: React.FC = observer(({ onMod - + + +
    ); @@ -173,7 +191,7 @@ export const ColumnsSelector: React.FC = observer(({ onMod alertGroupStore.columns = [...alertGroupStore.temporaryColumns]; } - function onItemChange(id: string | number) { + async function onItemChange(id: string | number) { const checkedItems = alertGroupStore.columns.filter((col) => col.isVisible); if (checkedItems.length === 1 && checkedItems[0].id === id) { openErrorNotification('At least one column should be selected'); @@ -184,6 +202,8 @@ export const ColumnsSelector: React.FC = observer(({ onMod let newItem: AGColumn = { ...item, isVisible: !item.isVisible }; return item.id === id ? newItem : item; }); + + await alertGroupStore.updateTableSettings(convertColumnsToTableSettings(alertGroupStore.columns), true); } function handleDragEnd(event: DragEndEvent, isVisible: boolean) { @@ -202,3 +222,12 @@ export const ColumnsSelector: React.FC = observer(({ onMod } } }); + +export function convertColumnsToTableSettings(columns: AGColumn[]): { visible: AGColumn[]; hidden: AGColumn[] } { + const tableSettings: { visible: AGColumn[]; hidden: AGColumn[] } = { + visible: columns.filter((col: AGColumn) => col.isVisible), + hidden: columns.filter((col: AGColumn) => !col.isVisible), + }; + + return tableSettings; +} diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx index d9e1ad9053..505e2e0b8a 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx @@ -14,7 +14,7 @@ import { import cn from 'classnames/bind'; import Text from 'components/Text/Text'; -import { ColumnsSelector } from 'containers/ColumnsSelector/ColumnsSelector'; +import { ColumnsSelector, convertColumnsToTableSettings } from 'containers/ColumnsSelector/ColumnsSelector'; import styles from 'containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss'; import { AGColumn, AGColumnType } from 'models/alertgroup/alertgroup.types'; import { Label } from 'models/label/label.types'; @@ -23,6 +23,8 @@ import { useDebouncedCallback } from 'utils/hooks'; import LoaderStore from 'models/loader/loader'; import { ActionKey } from 'models/loader/action-keys'; import { observer } from 'mobx-react'; +import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; +import { UserActions } from 'utils/authorization'; const cx = cn.bind(styles); @@ -31,7 +33,9 @@ interface ColumnsSelectorWrapperProps {} const DEBOUNCE_MS = 300; const ColumnsSelectorWrapper: React.FC = () => { - const [isModalOpen, setIsModalOpen] = useState(false); + const [isConfirmRemovalModalOpen, setIsConfirmRemovalModalOpen] = useState(false); + const [columnToBeRemoved, setColumnToBeRemoved] = useState(undefined); + const [isColumnAddModalOpen, setIsColumnAddModalOpen] = useState(false); const [labelKeys, setLabelKeys] = useState([]); @@ -40,25 +44,53 @@ const ColumnsSelectorWrapper: React.FC = () => { const store = useStore(); useEffect(() => { - isModalOpen && + isColumnAddModalOpen && (async function () { const keys = await store.alertGroupStore.loadLabelsKeys(); setLabelKeys(keys); })(); - }, [isModalOpen]); + }, [isColumnAddModalOpen]); return ( <> - {!isModalOpen ? ( + + + Are you sure you want to remove column label {columnToBeRemoved?.name}? + + + + + + + + + {!isColumnAddModalOpen && !isConfirmRemovalModalOpen ? ( setIsModalOpen(!isModalOpen)} />} + content={ + setIsColumnAddModalOpen(!isColumnAddModalOpen)} + onConfirmRemovalModalOpen={(column: AGColumn) => { + setIsConfirmRemovalModalOpen(!isConfirmRemovalModalOpen); + setColumnToBeRemoved(column); + }} + /> + } placement={'bottom-end'} show={true} closeButton={false} @@ -72,6 +104,21 @@ const ColumnsSelectorWrapper: React.FC = () => { ); + function onConfirmRemovalClose(): void { + setIsConfirmRemovalModalOpen(false); + forceOpenToggletip(); + } + + async function onColumnRemovalClick(): Promise { + const columns = store.alertGroupStore.columns.filter((col) => col.id !== columnToBeRemoved.id); + + await store.alertGroupStore.updateTableSettings(convertColumnsToTableSettings(columns), false); + await store.alertGroupStore.fetchTableSettings(); + + setIsConfirmRemovalModalOpen(false); + forceOpenToggletip(); + } + function renderToggletipButton() { return ( - + + + ); - async function onAddNewColumns() { - const newColumns: AGColumn[] = searchResults - .filter((item) => item.isChecked) - .map( - (it): AGColumn => ({ - id: it.id, - name: it.name, - isVisible: false, - type: AGColumnType.LABEL, - }) - ); + function onCloseModal() { + inputRef.current.value = ''; - const allColumns = [...store.alertGroupStore.columns, ...newColumns]; + setSearchResults([]); + setIsModalOpen(false); + setTimeout(() => forceOpenToggletip(), 0); + } - await store.alertGroupStore.updateTableSettings(allColumns, false); + async function onAddNewColumns() { + const mergedColumns = [ + ...store.alertGroupStore.columns, + ...searchResults + .filter((item) => item.isChecked) + .map( + (it): AGColumn => ({ + id: it.id, + name: it.name, + isVisible: false, + type: AGColumnType.LABEL, + }) + ), + ]; + + const columns: { visible: AGColumn[]; hidden: AGColumn[] } = { + visible: mergedColumns.filter((col) => col.isVisible), + hidden: mergedColumns.filter((col) => !col.isVisible), + }; + + await store.alertGroupStore.updateTableSettings(columns, false); + await store.alertGroupStore.fetchTableSettings(); setIsModalOpen(false); setTimeout(() => forceOpenToggletip(), 0); @@ -203,10 +258,6 @@ const ColumnsModal: React.FC = observer(({ isModalOpen, label inputRef.current.value = ''; } - function forceOpenToggletip() { - document.getElementById('toggletip-button')?.click(); - } - function onInputChange() { const search = inputRef?.current?.value; setSearchResults( @@ -215,4 +266,8 @@ const ColumnsModal: React.FC = observer(({ isModalOpen, label } }); +function forceOpenToggletip() { + document.getElementById('toggletip-button')?.click(); +} + export default ColumnsSelectorWrapper; diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.ts b/grafana-plugin/src/models/alertgroup/alertgroup.ts index 2b1adc5240..e09f6b6188 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.ts @@ -447,8 +447,6 @@ export class AlertGroupStore extends BaseStore { }).catch(this.onApiError); } - // api/internal/v1/alertgroup_table_settings - @action public async fetchTableSettings(): Promise { const tableSettings = await makeRequest('/alertgroup_table_settings', {}); @@ -463,12 +461,15 @@ export class AlertGroupStore extends BaseStore { @action @AutoLoadingState(ActionKey.IS_ADDING_NEW_COLUMN_TO_ALERT_GROUP) - public async updateTableSettings(columns: AGColumn[], isUserUpdate: boolean): Promise { + public async updateTableSettings( + columns: { visible: AGColumn[]; hidden: AGColumn[] }, + isUserUpdate: boolean + ): Promise { const method = isUserUpdate ? 'PUT' : 'POST'; await makeRequest('/alertgroup_table_settings', { method, - data: [...columns], + data: { ...columns }, }); } diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index fe92fa735a..d27df59240 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -660,9 +660,9 @@ class Incidents extends React.Component key: 'alerts', render: this.renderAlertsCounter, }, - Source: { + Integration: { title: 'Integration', - key: 'source', + key: 'integration', render: this.renderSource, }, Title: { diff --git a/grafana-plugin/src/pages/integration/Integration.tsx b/grafana-plugin/src/pages/integration/Integration.tsx index 4c028d3abc..fad5655104 100644 --- a/grafana-plugin/src/pages/integration/Integration.tsx +++ b/grafana-plugin/src/pages/integration/Integration.tsx @@ -941,7 +941,7 @@ const IntegrationActions: React.FC = ({ onClick={() => { setConfirmModal({ isOpen: true, - title: 'Delete Integration?', + title: 'Delete Integration', body: ( Are you sure you want to delete ? From 0f8d1071544eed4fd4dad2473509d3a0d4672eb1 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Fri, 17 Nov 2023 16:33:16 +0200 Subject: [PATCH 30/67] lint --- grafana-plugin/src/assets/style/utils.css | 4 +- .../ColumnsSelector/ColumnsSelector.tsx | 224 +++++++++--------- .../ColumnsSelectorWrapper.tsx | 10 +- .../src/models/alertgroup/alertgroup.ts | 4 +- 4 files changed, 122 insertions(+), 120 deletions(-) diff --git a/grafana-plugin/src/assets/style/utils.css b/grafana-plugin/src/assets/style/utils.css index 801bb542cf..09407b3ceb 100644 --- a/grafana-plugin/src/assets/style/utils.css +++ b/grafana-plugin/src/assets/style/utils.css @@ -117,9 +117,9 @@ * Icons */ - .loader { +.loader { margin-bottom: 0 !important; - } +} .icon-exclamation { color: var(--error-text-color); diff --git a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx index 1744543f36..c8be466d66 100644 --- a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx +++ b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx @@ -25,10 +25,10 @@ import { CSSTransition, TransitionGroup } from 'react-transition-group'; import Text from 'components/Text/Text'; import styles from 'containers/ColumnsSelector/ColumnsSelector.module.scss'; +import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; import { AGColumn, AGColumnType } from 'models/alertgroup/alertgroup.types'; import { useStore } from 'state/useStore'; import { openErrorNotification } from 'utils'; -import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; import { UserActions } from 'utils/authorization'; const cx = cn.bind(styles); @@ -97,131 +97,133 @@ interface ColumnsSelectorProps { onConfirmRemovalModalOpen(column: AGColumn): void; } -export const ColumnsSelector: React.FC = observer(({ onColumnAddModalOpen, onConfirmRemovalModalOpen }) => { - const { alertGroupStore } = useStore(); - const { columns: items, temporaryColumns } = alertGroupStore; - - const visibleColumns = items.filter((col) => col.isVisible); - const hiddenColumns = items.filter((col) => !col.isVisible); - - useEffect(() => { - if (!temporaryColumns.length) { - alertGroupStore.temporaryColumns = cloneDeep(items); - } - }, []); - - const canResetData = useMemo( - () => !isEqual(temporaryColumns, items), - [alertGroupStore.columns, alertGroupStore.temporaryColumns] - ); - - const sensors = useSensors( - useSensor(PointerSensor), - useSensor(KeyboardSensor, { - coordinateGetter: sortableKeyboardCoordinates, - }) - ); - - return ( -
    - - Fields Settings - - -
    - - Visible ({visibleColumns.length}) - - - handleDragEnd(ev, true)}> - - - {visibleColumns.map((column) => ( - - - - ))} - - - -
    - -
    - - Hidden ({hiddenColumns.length}) +export const ColumnsSelector: React.FC = observer( + ({ onColumnAddModalOpen, onConfirmRemovalModalOpen }) => { + const { alertGroupStore } = useStore(); + const { columns: items, temporaryColumns } = alertGroupStore; + + const visibleColumns = items.filter((col) => col.isVisible); + const hiddenColumns = items.filter((col) => !col.isVisible); + + useEffect(() => { + if (!temporaryColumns.length) { + alertGroupStore.temporaryColumns = cloneDeep(items); + } + }, []); + + const canResetData = useMemo( + () => !isEqual(temporaryColumns, items), + [alertGroupStore.columns, alertGroupStore.temporaryColumns] + ); + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + return ( +
    + + Fields Settings - handleDragEnd(ev, false)}> - - - {hiddenColumns.map((column) => ( - - - - ))} - - - -
    - -
    - - - - + + + +
    -
    - ); - - function onReset() { - alertGroupStore.columns = [...alertGroupStore.temporaryColumns]; - } + ); - async function onItemChange(id: string | number) { - const checkedItems = alertGroupStore.columns.filter((col) => col.isVisible); - if (checkedItems.length === 1 && checkedItems[0].id === id) { - openErrorNotification('At least one column should be selected'); - return; + function onReset() { + alertGroupStore.columns = [...alertGroupStore.temporaryColumns]; } - alertGroupStore.columns = alertGroupStore.columns.map((item): AGColumn => { - let newItem: AGColumn = { ...item, isVisible: !item.isVisible }; - return item.id === id ? newItem : item; - }); + async function onItemChange(id: string | number) { + const checkedItems = alertGroupStore.columns.filter((col) => col.isVisible); + if (checkedItems.length === 1 && checkedItems[0].id === id) { + openErrorNotification('At least one column should be selected'); + return; + } - await alertGroupStore.updateTableSettings(convertColumnsToTableSettings(alertGroupStore.columns), true); - } + alertGroupStore.columns = alertGroupStore.columns.map((item): AGColumn => { + let newItem: AGColumn = { ...item, isVisible: !item.isVisible }; + return item.id === id ? newItem : item; + }); + + await alertGroupStore.updateTableSettings(convertColumnsToTableSettings(alertGroupStore.columns), true); + } - function handleDragEnd(event: DragEndEvent, isVisible: boolean) { - const { active, over } = event; + function handleDragEnd(event: DragEndEvent, isVisible: boolean) { + const { active, over } = event; - let searchableList: AGColumn[] = isVisible ? visibleColumns : hiddenColumns; + let searchableList: AGColumn[] = isVisible ? visibleColumns : hiddenColumns; - if (active.id !== over.id) { - const oldIndex = searchableList.findIndex((item) => item.id === active.id); - const newIndex = searchableList.findIndex((item) => item.id === over.id); + if (active.id !== over.id) { + const oldIndex = searchableList.findIndex((item) => item.id === active.id); + const newIndex = searchableList.findIndex((item) => item.id === over.id); - searchableList = arrayMove(searchableList, oldIndex, newIndex); + searchableList = arrayMove(searchableList, oldIndex, newIndex); - const updatedList = isVisible ? [...searchableList, ...hiddenColumns] : [...visibleColumns, ...searchableList]; - alertGroupStore.columns = updatedList; + const updatedList = isVisible ? [...searchableList, ...hiddenColumns] : [...visibleColumns, ...searchableList]; + alertGroupStore.columns = updatedList; + } } } -}); +); export function convertColumnsToTableSettings(columns: AGColumn[]): { visible: AGColumn[]; hidden: AGColumn[] } { const tableSettings: { visible: AGColumn[]; hidden: AGColumn[] } = { diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx index 505e2e0b8a..faaed30362 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx @@ -12,19 +12,19 @@ import { VerticalGroup, } from '@grafana/ui'; import cn from 'classnames/bind'; +import { observer } from 'mobx-react'; import Text from 'components/Text/Text'; import { ColumnsSelector, convertColumnsToTableSettings } from 'containers/ColumnsSelector/ColumnsSelector'; import styles from 'containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss'; +import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; import { AGColumn, AGColumnType } from 'models/alertgroup/alertgroup.types'; import { Label } from 'models/label/label.types'; -import { useStore } from 'state/useStore'; -import { useDebouncedCallback } from 'utils/hooks'; -import LoaderStore from 'models/loader/loader'; import { ActionKey } from 'models/loader/action-keys'; -import { observer } from 'mobx-react'; -import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; +import LoaderStore from 'models/loader/loader'; +import { useStore } from 'state/useStore'; import { UserActions } from 'utils/authorization'; +import { useDebouncedCallback } from 'utils/hooks'; const cx = cn.bind(styles); diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.ts b/grafana-plugin/src/models/alertgroup/alertgroup.ts index e09f6b6188..f90e8c574b 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.ts @@ -4,6 +4,7 @@ import qs from 'query-string'; import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; import BaseStore from 'models/base_store'; import { Label, LabelKey } from 'models/label/label.types'; +import { ActionKey } from 'models/loader/action-keys'; import { User } from 'models/user/user.types'; import { makeRequest } from 'network'; import { Mixpanel } from 'services/mixpanel'; @@ -11,10 +12,9 @@ import { RootStore } from 'state'; import { SelectOption } from 'state/types'; import { openErrorNotification, refreshPageError, showApiError } from 'utils'; import LocationHelper from 'utils/LocationHelper'; +import { AutoLoadingState } from 'utils/decorators'; import { AGColumn, Alert, AlertAction, IncidentStatus } from './alertgroup.types'; -import { AutoLoadingState } from 'utils/decorators'; -import { ActionKey } from 'models/loader/action-keys'; export class AlertGroupStore extends BaseStore { @observable.shallow From f0dc47b9eec001c33809c696de41d1111cb0d6ea Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Fri, 17 Nov 2023 16:46:23 +0200 Subject: [PATCH 31/67] refactored modal to separate file --- .../ColumnsSelector/ColumnsSelector.tsx | 1 + .../ColumnsSelectorWrapper/ColumnsModal.tsx | 152 ++++++++++++++++++ .../ColumnsSelectorWrapper.module.scss | 4 + .../ColumnsSelectorWrapper.tsx | 152 +----------------- 4 files changed, 161 insertions(+), 148 deletions(-) create mode 100644 grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx diff --git a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx index c8be466d66..4659142d1c 100644 --- a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx +++ b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx @@ -76,6 +76,7 @@ const ColumnRow: React.FC = ({ column, onItemChange, onColumnRem className={cx('column-icon', 'column-icon--trash')} name="trash-alt" aria-label="Remove" + tooltip={'Remove column'} onClick={() => onColumnRemoval(column)} /> diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx new file mode 100644 index 0000000000..79c1c57aaf --- /dev/null +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx @@ -0,0 +1,152 @@ +import cn from 'classnames/bind'; +import { observer } from 'mobx-react'; +import { Label } from 'models/label/label.types'; +import React, { useState } from 'react'; +import { useStore } from 'state/useStore'; +import { useDebouncedCallback } from 'utils/hooks'; +import LoaderStore from 'models/loader/loader'; +import { ActionKey } from 'models/loader/action-keys'; +import { Button, Checkbox, HorizontalGroup, Input, LoadingPlaceholder, Modal, VerticalGroup } from '@grafana/ui'; +import styles from 'containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss'; +import Text from 'components/Text/Text'; +import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; +import { UserActions } from 'utils/authorization'; +import { AGColumn, AGColumnType } from 'models/alertgroup/alertgroup.types'; + +const cx = cn.bind(styles); + +interface ColumnsModalProps { + isModalOpen: boolean; + labelKeys: Label[]; + setIsModalOpen: (value: boolean) => void; + inputRef: React.RefObject; +} + +interface SearchResult extends Label { + isChecked: boolean; +} + +const DEBOUNCE_MS = 300; + +export const ColumnsModal: React.FC = observer( + ({ isModalOpen, labelKeys, setIsModalOpen, inputRef }) => { + const store = useStore(); + const [searchResults, setSearchResults] = useState([]); + const debouncedOnInputChange = useDebouncedCallback(onInputChange, DEBOUNCE_MS); + + const isLoading = LoaderStore.isLoading(ActionKey.IS_ADDING_NEW_COLUMN_TO_ALERT_GROUP); + + return ( + + +
    + + + + {inputRef?.current?.value === '' && ( + {labelKeys.length} items available. Type to see suggestions + )} + + {inputRef?.current?.value && searchResults.length && ( + + {searchResults.map((result, index) => ( +
    + { + setSearchResults((items) => { + return items.map((item) => { + const updatedItem: SearchResult = { ...item, isChecked: !item.isChecked }; + return item.id === result.id ? updatedItem : item; + }); + }); + }} + /> + + {result.name} +
    + ))} +
    + )} + + {inputRef?.current?.value && searchResults.length === 0 && ( + 0 results for your search. + )} +
    +
    + + + + + + + +
    +
    + ); + + function onCloseModal() { + inputRef.current.value = ''; + + setSearchResults([]); + setIsModalOpen(false); + setTimeout(forceOpenToggletip, 0); + } + + async function onAddNewColumns() { + const mergedColumns = [ + ...store.alertGroupStore.columns, + ...searchResults + .filter((item) => item.isChecked) + .map( + (it): AGColumn => ({ + id: it.id, + name: it.name, + isVisible: false, + type: AGColumnType.LABEL, + }) + ), + ]; + + const columns: { visible: AGColumn[]; hidden: AGColumn[] } = { + visible: mergedColumns.filter((col) => col.isVisible), + hidden: mergedColumns.filter((col) => !col.isVisible), + }; + + await store.alertGroupStore.updateTableSettings(columns, false); + await store.alertGroupStore.fetchTableSettings(); + + setIsModalOpen(false); + setTimeout(() => forceOpenToggletip(), 0); + setSearchResults([]); + + inputRef.current.value = ''; + } + + function onInputChange() { + const search = inputRef?.current?.value; + setSearchResults( + labelKeys.filter((pair) => pair.name.indexOf(search) > -1).map((pair) => ({ ...pair, isChecked: false })) + ); + } + + function forceOpenToggletip() { + document.getElementById('toggletip-button')?.click(); + } + } +); diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss index 64e45b5e1d..499621e728 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss @@ -14,3 +14,7 @@ width: 100%; min-height: 100px; } + +.removal-modal { + max-width: 500px; +} \ No newline at end of file diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx index faaed30362..8300d65164 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx @@ -1,37 +1,20 @@ import React, { useEffect, useRef, useState } from 'react'; -import { - Button, - Checkbox, - HorizontalGroup, - Icon, - Input, - LoadingPlaceholder, - Modal, - Toggletip, - VerticalGroup, -} from '@grafana/ui'; +import { Button, HorizontalGroup, Icon, Modal, Toggletip, VerticalGroup } from '@grafana/ui'; import cn from 'classnames/bind'; -import { observer } from 'mobx-react'; import Text from 'components/Text/Text'; import { ColumnsSelector, convertColumnsToTableSettings } from 'containers/ColumnsSelector/ColumnsSelector'; import styles from 'containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss'; -import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; -import { AGColumn, AGColumnType } from 'models/alertgroup/alertgroup.types'; +import { AGColumn } from 'models/alertgroup/alertgroup.types'; import { Label } from 'models/label/label.types'; -import { ActionKey } from 'models/loader/action-keys'; -import LoaderStore from 'models/loader/loader'; import { useStore } from 'state/useStore'; -import { UserActions } from 'utils/authorization'; -import { useDebouncedCallback } from 'utils/hooks'; +import { ColumnsModal } from './ColumnsModal'; const cx = cn.bind(styles); interface ColumnsSelectorWrapperProps {} -const DEBOUNCE_MS = 300; - const ColumnsSelectorWrapper: React.FC = () => { const [isConfirmRemovalModalOpen, setIsConfirmRemovalModalOpen] = useState(false); const [columnToBeRemoved, setColumnToBeRemoved] = useState(undefined); @@ -65,6 +48,7 @@ const ColumnsSelectorWrapper: React.FC = () => { isOpen={isConfirmRemovalModalOpen} title={'Remove column'} onDismiss={onConfirmRemovalClose} + className={cx('removal-modal')} > Are you sure you want to remove column label {columnToBeRemoved?.name}? @@ -138,134 +122,6 @@ const ColumnsSelectorWrapper: React.FC = () => { } }; -interface ColumnsModalProps { - isModalOpen: boolean; - labelKeys: Label[]; - setIsModalOpen: (value: boolean) => void; - inputRef: React.RefObject; -} - -interface SearchResult extends Label { - isChecked: boolean; -} - -const ColumnsModal: React.FC = observer(({ isModalOpen, labelKeys, setIsModalOpen, inputRef }) => { - const store = useStore(); - const [searchResults, setSearchResults] = useState([]); - const debouncedOnInputChange = useDebouncedCallback(onInputChange, DEBOUNCE_MS); - - const isLoading = LoaderStore.isLoading(ActionKey.IS_ADDING_NEW_COLUMN_TO_ALERT_GROUP); - - return ( - - -
    - - - - {inputRef?.current?.value === '' && ( - {labelKeys.length} items available. Type to see suggestions - )} - - {inputRef?.current?.value && searchResults.length && ( - - {searchResults.map((result, index) => ( -
    - { - setSearchResults((items) => { - return items.map((item) => { - const updatedItem: SearchResult = { ...item, isChecked: !item.isChecked }; - return item.id === result.id ? updatedItem : item; - }); - }); - }} - /> - - {result.name} -
    - ))} -
    - )} - - {inputRef?.current?.value && searchResults.length === 0 && ( - 0 results for your search. - )} -
    -
    - - - - - - - -
    -
    - ); - - function onCloseModal() { - inputRef.current.value = ''; - - setSearchResults([]); - setIsModalOpen(false); - setTimeout(() => forceOpenToggletip(), 0); - } - - async function onAddNewColumns() { - const mergedColumns = [ - ...store.alertGroupStore.columns, - ...searchResults - .filter((item) => item.isChecked) - .map( - (it): AGColumn => ({ - id: it.id, - name: it.name, - isVisible: false, - type: AGColumnType.LABEL, - }) - ), - ]; - - const columns: { visible: AGColumn[]; hidden: AGColumn[] } = { - visible: mergedColumns.filter((col) => col.isVisible), - hidden: mergedColumns.filter((col) => !col.isVisible), - }; - - await store.alertGroupStore.updateTableSettings(columns, false); - await store.alertGroupStore.fetchTableSettings(); - - setIsModalOpen(false); - setTimeout(() => forceOpenToggletip(), 0); - setSearchResults([]); - - inputRef.current.value = ''; - } - - function onInputChange() { - const search = inputRef?.current?.value; - setSearchResults( - labelKeys.filter((pair) => pair.name.indexOf(search) > -1).map((pair) => ({ ...pair, isChecked: false })) - ); - } -}); - function forceOpenToggletip() { document.getElementById('toggletip-button')?.click(); } From cf6826a2ae82baad9f49c81589be8be796d74aa4 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Fri, 17 Nov 2023 16:46:30 +0200 Subject: [PATCH 32/67] linter --- .../containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx index 8300d65164..2534d04850 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx @@ -9,6 +9,7 @@ import styles from 'containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.mod import { AGColumn } from 'models/alertgroup/alertgroup.types'; import { Label } from 'models/label/label.types'; import { useStore } from 'state/useStore'; + import { ColumnsModal } from './ColumnsModal'; const cx = cn.bind(styles); From b7d687e5b01082b7e2afb5f4c7ab3dfdacce92b5 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Fri, 17 Nov 2023 17:03:18 +0200 Subject: [PATCH 33/67] changed renderLabels to arrow function --- grafana-plugin/src/pages/incidents/Incidents.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index 46bc7a97d0..dd49bc4171 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -607,7 +607,7 @@ class Incidents extends React.Component ); } - renderLabels(item: AlertType) { + renderLabels = (item: AlertType) => { if (!item.labels.length) { return null; } @@ -636,7 +636,7 @@ class Incidents extends React.Component } /> ); - } + }; renderTeam(record: AlertType, teams: any) { return ( From 4ed0caa52a60ca97b5803af90adf07321ad8982e Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Fri, 17 Nov 2023 17:04:05 +0200 Subject: [PATCH 34/67] lint --- .../ColumnsSelectorWrapper/ColumnsModal.tsx | 22 ++++++++++--------- .../src/pages/incidents/Incidents.tsx | 2 +- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx index 79c1c57aaf..dfec12656b 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx @@ -1,17 +1,19 @@ -import cn from 'classnames/bind'; -import { observer } from 'mobx-react'; -import { Label } from 'models/label/label.types'; import React, { useState } from 'react'; -import { useStore } from 'state/useStore'; -import { useDebouncedCallback } from 'utils/hooks'; -import LoaderStore from 'models/loader/loader'; -import { ActionKey } from 'models/loader/action-keys'; + import { Button, Checkbox, HorizontalGroup, Input, LoadingPlaceholder, Modal, VerticalGroup } from '@grafana/ui'; -import styles from 'containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss'; +import cn from 'classnames/bind'; +import { observer } from 'mobx-react'; + import Text from 'components/Text/Text'; +import styles from 'containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; -import { UserActions } from 'utils/authorization'; import { AGColumn, AGColumnType } from 'models/alertgroup/alertgroup.types'; +import { Label } from 'models/label/label.types'; +import { ActionKey } from 'models/loader/action-keys'; +import LoaderStore from 'models/loader/loader'; +import { useStore } from 'state/useStore'; +import { UserActions } from 'utils/authorization'; +import { useDebouncedCallback } from 'utils/hooks'; const cx = cn.bind(styles); @@ -146,7 +148,7 @@ export const ColumnsModal: React.FC = observer( } function forceOpenToggletip() { - document.getElementById('toggletip-button')?.click(); + document.getElementById('toggletip-button')?.click(); } } ); diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index dd49bc4171..74e38a5c2c 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -33,6 +33,7 @@ import { AGColumn, AGColumnType, } from 'models/alertgroup/alertgroup.types'; +import { LabelKeyValue } from 'models/label/label.types'; import { renderRelatedUsers } from 'pages/incident/Incident.helpers'; import { AppFeature } from 'state/features'; import { PageProps, WithStoreProps } from 'state/types'; @@ -45,7 +46,6 @@ import { TableColumn } from 'utils/types'; import styles from './Incidents.module.scss'; import { IncidentDropdown } from './parts/IncidentDropdown'; import { SilenceButtonCascader } from './parts/SilenceButtonCascader'; -import { LabelKeyValue } from 'models/label/label.types'; const cx = cn.bind(styles); From 7b6141731396c7e44c259dd64ac194c5e3031282 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 20 Nov 2023 11:22:20 +0100 Subject: [PATCH 35/67] Update validation for columns settings endpoint --- .../api/serializers/alert_group_table_settings.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/engine/apps/api/serializers/alert_group_table_settings.py b/engine/apps/api/serializers/alert_group_table_settings.py index c3ce1acda7..6c3ce04aa7 100644 --- a/engine/apps/api/serializers/alert_group_table_settings.py +++ b/engine/apps/api/serializers/alert_group_table_settings.py @@ -38,25 +38,27 @@ class AlertGroupTableColumnsListSerializer(serializers.Serializer): def validate(self, data): """ - Validate data regarding if it updates alert group table columns settings for organization or for user: + Validate data regarding if it updates alert group table columns settings for organization or for user + and validate that at least one column is selected as visible. `is_org_settings=True` means that organization alert group table columns list should be updated. Validate that all default columns are in the list. `is_org_settings=False` means that list of visible columns for user should be updated. - Validate that all columns exist in organization alert group table columns list and at least one column is - selected as visible. + Validate that all columns exist in organization alert group table columns list. """ is_org_settings = self.context.get("is_org_settings") is True organization = self.context["request"].auth.organization columns_list = data["visible"] + data["hidden"] request_columns_ids = [column["id"] for column in columns_list] + if len(data["visible"]) == 0: + raise ValidationError("At least one column should be selected as visible") if is_org_settings: if not set(request_columns_ids) >= set(AlertGroupTableDefaultColumnChoices.values): raise ValidationError("Default column cannot be removed") + elif len(request_columns_ids) > len(set(request_columns_ids)): + raise ValidationError("Duplicate column") else: - if len(data["visible"]) == 0: - raise ValidationError("At least one column should be selected as visible") organization_columns_ids = [column["id"] for column in organization.alert_group_table_columns] if set(organization_columns_ids) != set(request_columns_ids): raise ValidationError("Invalid settings") From 6af37d06845294d203dddf1ab8113fa5adf6cdb2 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 20 Nov 2023 12:35:34 +0200 Subject: [PATCH 36/67] a few more improvements --- .../ColumnsSelectorWrapper/ColumnsModal.tsx | 34 ++++++++++++++----- .../ColumnsSelectorWrapper.tsx | 2 +- grafana-plugin/src/utils/index.ts | 4 +++ 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx index dfec12656b..ddd533d0fb 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { Button, Checkbox, HorizontalGroup, Input, LoadingPlaceholder, Modal, VerticalGroup } from '@grafana/ui'; import cn from 'classnames/bind'; @@ -12,6 +12,7 @@ import { Label } from 'models/label/label.types'; import { ActionKey } from 'models/loader/action-keys'; import LoaderStore from 'models/loader/loader'; import { useStore } from 'state/useStore'; +import { openErrorNotification, pluralize } from 'utils'; import { UserActions } from 'utils/authorization'; import { useDebouncedCallback } from 'utils/hooks'; @@ -38,6 +39,11 @@ export const ColumnsModal: React.FC = observer( const isLoading = LoaderStore.isLoading(ActionKey.IS_ADDING_NEW_COLUMN_TO_ALERT_GROUP); + const availableKeysForSearching = useMemo(() => { + const currentAGColumns = store.alertGroupStore.columns.map((col) => col.name); + return labelKeys.filter((pair) => currentAGColumns.indexOf(pair.name) === -1); + }, [labelKeys, store.alertGroupStore.columns]); + return ( @@ -52,7 +58,10 @@ export const ColumnsModal: React.FC = observer( /> {inputRef?.current?.value === '' && ( - {labelKeys.length} items available. Type to see suggestions + + {availableKeysForSearching.length} {pluralize('item', availableKeysForSearching.length)} available. + Type to see suggestions + )} {inputRef?.current?.value && searchResults.length && ( @@ -130,20 +139,27 @@ export const ColumnsModal: React.FC = observer( hidden: mergedColumns.filter((col) => !col.isVisible), }; - await store.alertGroupStore.updateTableSettings(columns, false); - await store.alertGroupStore.fetchTableSettings(); + try { + await store.alertGroupStore.updateTableSettings(columns, false); + await store.alertGroupStore.fetchTableSettings(); - setIsModalOpen(false); - setTimeout(() => forceOpenToggletip(), 0); - setSearchResults([]); + setIsModalOpen(false); + setTimeout(() => forceOpenToggletip(), 0); + setSearchResults([]); - inputRef.current.value = ''; + inputRef.current.value = ''; + } catch (ex) { + openErrorNotification('An error has occurred. Please try again'); + } } function onInputChange() { const search = inputRef?.current?.value; + setSearchResults( - labelKeys.filter((pair) => pair.name.indexOf(search) > -1).map((pair) => ({ ...pair, isChecked: false })) + availableKeysForSearching + .filter((pair) => pair.name.indexOf(search) > -1) + .map((pair) => ({ ...pair, isChecked: false })) ); } diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx index 2534d04850..4c832aa45f 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx @@ -51,7 +51,7 @@ const ColumnsSelectorWrapper: React.FC = () => { onDismiss={onConfirmRemovalClose} className={cx('removal-modal')} > - + Are you sure you want to remove column label {columnToBeRemoved?.name}? diff --git a/grafana-plugin/src/utils/index.ts b/grafana-plugin/src/utils/index.ts index d3e4cd5a68..27e477ae0a 100644 --- a/grafana-plugin/src/utils/index.ts +++ b/grafana-plugin/src/utils/index.ts @@ -91,3 +91,7 @@ export function getPaths(obj?: any, parentKey?: string): string[] { } return concat(result, parentKey || []); } + +export function pluralize(word: string, count: number): string { + return count === 1 ? word : `${word}s`; +} From 895387cef6a016d21fa0b87b12bbe0acc137ab99 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 20 Nov 2023 14:24:41 +0100 Subject: [PATCH 37/67] Add tests --- .../tests/test_alert_group_table_settings.py | 210 ++++++++++++++++++ .../tests/test_alert_group_table_settings.py | 88 ++++++++ 2 files changed, 298 insertions(+) create mode 100644 engine/apps/api/tests/test_alert_group_table_settings.py create mode 100644 engine/apps/user_management/tests/test_alert_group_table_settings.py diff --git a/engine/apps/api/tests/test_alert_group_table_settings.py b/engine/apps/api/tests/test_alert_group_table_settings.py new file mode 100644 index 0000000000..4ae106484d --- /dev/null +++ b/engine/apps/api/tests/test_alert_group_table_settings.py @@ -0,0 +1,210 @@ +import pytest +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APIClient + +from apps.api.permissions import LegacyAccessControlRole +from apps.user_management.constants import AlertGroupTableColumnTypeChoices +from apps.user_management.utils import alert_group_table_user_settings, default_columns + +DEFAULT_COLUMNS = default_columns() + + +def columns_settings(add_column=None): + default_settings = {"visible": DEFAULT_COLUMNS[:], "hidden": []} + if add_column: + default_settings["hidden"].append(add_column) + return default_settings + + +@pytest.mark.django_db +def test_get_columns( + make_organization_and_user_with_plugin_token, + make_user_auth_headers, +): + organization, user, token = make_organization_and_user_with_plugin_token() + client = APIClient() + url = reverse("api-internal:alert_group_table-columns_settings") + response = client.get(url, format="json", **make_user_auth_headers(user, token)) + expected_result = alert_group_table_user_settings(user) + + assert response.status_code == status.HTTP_200_OK + assert response.json() == expected_result + + +@pytest.mark.parametrize( + "initial_columns_settings,updated_columns_settings,status_code", + [ + # add column + ( + columns_settings(), + columns_settings({"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}), + status.HTTP_200_OK, + ), + # remove column + ( + columns_settings({"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}), + columns_settings(), + status.HTTP_200_OK, + ), + # wrong data format + (columns_settings(), {}, status.HTTP_400_BAD_REQUEST), + (columns_settings(), {"visible": []}, status.HTTP_400_BAD_REQUEST), + (columns_settings(), {"hidden": []}, status.HTTP_400_BAD_REQUEST), + # wrong id + ( + columns_settings(), + columns_settings({"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.DEFAULT.value}), + status.HTTP_400_BAD_REQUEST, + ), + # duplicate id + ( + columns_settings(), + columns_settings({"name": "Test", "id": 1, "type": AlertGroupTableColumnTypeChoices.DEFAULT.value}), + status.HTTP_400_BAD_REQUEST, + ), + # remove default column + ( + columns_settings(), + {"visible": DEFAULT_COLUMNS[:-1], "hidden": []}, + status.HTTP_400_BAD_REQUEST, + ), + ], +) +@pytest.mark.django_db +def test_update_columns_list( + initial_columns_settings, + updated_columns_settings, + status_code, + make_organization_and_user_with_plugin_token, + make_user_auth_headers, +): + """Test alert group table settings for organization (POST request)""" + organization, user, token = make_organization_and_user_with_plugin_token() + client = APIClient() + url = reverse("api-internal:alert_group_table-columns_settings") + client.post(url, data=initial_columns_settings, format="json", **make_user_auth_headers(user, token)) + response = client.post(url, data=updated_columns_settings, format="json", **make_user_auth_headers(user, token)) + assert response.status_code == status_code + if status_code == status.HTTP_200_OK: + assert response.json() == updated_columns_settings + + +@pytest.mark.parametrize( + "initial_columns_settings,updated_columns_settings,status_code", + [ + # hide column + (columns_settings(), {"visible": DEFAULT_COLUMNS[:-1], "hidden": DEFAULT_COLUMNS[-1:]}, status.HTTP_200_OK), + # make column visible + ({"visible": DEFAULT_COLUMNS[:-1], "hidden": DEFAULT_COLUMNS[-1:]}, columns_settings(), status.HTTP_200_OK), + # wrong data format + (columns_settings(), {}, status.HTTP_400_BAD_REQUEST), + (columns_settings(), {"visible": []}, status.HTTP_400_BAD_REQUEST), + (columns_settings(), {"hidden": []}, status.HTTP_400_BAD_REQUEST), + # hide all columns + (columns_settings(), {"visible": [], "hidden": DEFAULT_COLUMNS[:]}, status.HTTP_400_BAD_REQUEST), + # add column + ( + columns_settings(), + columns_settings({"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}), + status.HTTP_400_BAD_REQUEST, + ), + # remove column + ( + columns_settings({"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}), + columns_settings(), + status.HTTP_400_BAD_REQUEST, + ), + ], +) +@pytest.mark.django_db +def test_update_columns_settings( + initial_columns_settings, + updated_columns_settings, + status_code, + make_organization_and_user_with_plugin_token, + make_user_auth_headers, +): + """Test alert group table settings for user (PUT request)""" + organization, user, token = make_organization_and_user_with_plugin_token() + client = APIClient() + url = reverse("api-internal:alert_group_table-columns_settings") + client.post(url, data=initial_columns_settings, format="json", **make_user_auth_headers(user, token)) + response = client.put(url, data=updated_columns_settings, format="json", **make_user_auth_headers(user, token)) + assert response.status_code == status_code + if status_code == status.HTTP_200_OK: + assert response.json() == updated_columns_settings + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "role,expected_status", + [ + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN), + ], +) +def test_get_columns_permissions( + role, + expected_status, + make_organization_and_user_with_plugin_token, + make_user_auth_headers, +): + organization, user, token = make_organization_and_user_with_plugin_token(role) + client = APIClient() + url = reverse("api-internal:alert_group_table-columns_settings") + response = client.get(url, format="json", **make_user_auth_headers(user, token)) + + assert response.status_code == expected_status + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "role,expected_status", + [ + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN), + ], +) +def test_update_columns_list_permissions( + role, + expected_status, + make_organization_and_user_with_plugin_token, + make_user_auth_headers, +): + organization, user, token = make_organization_and_user_with_plugin_token(role) + client = APIClient() + url = reverse("api-internal:alert_group_table-columns_settings") + data = columns_settings() + response = client.post(url, data=data, format="json", **make_user_auth_headers(user, token)) + + assert response.status_code == expected_status + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "role,expected_status", + [ + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN), + ], +) +def test_update_columns_settings_permissions( + role, + expected_status, + make_organization_and_user_with_plugin_token, + make_user_auth_headers, +): + organization, user, token = make_organization_and_user_with_plugin_token(role) + client = APIClient() + url = reverse("api-internal:alert_group_table-columns_settings") + data = columns_settings() + response = client.put(url, data=data, format="json", **make_user_auth_headers(user, token)) + + assert response.status_code == expected_status diff --git a/engine/apps/user_management/tests/test_alert_group_table_settings.py b/engine/apps/user_management/tests/test_alert_group_table_settings.py new file mode 100644 index 0000000000..c46ff2d79e --- /dev/null +++ b/engine/apps/user_management/tests/test_alert_group_table_settings.py @@ -0,0 +1,88 @@ +import pytest + +from apps.user_management.constants import AlertGroupTableColumnTypeChoices +from apps.user_management.utils import alert_group_table_user_settings, default_columns + +DEFAULT_COLUMNS = default_columns() + + +@pytest.mark.parametrize( + "user_settings,organization_settings,expected_result", + [ + # user doesn't have settings, organization has default settings - all columns are visible + ( + None, + DEFAULT_COLUMNS, + {"visible": DEFAULT_COLUMNS, "hidden": []}, + ), + # user doesn't have settings, organization has updated settings - only default columns are visible + ( + None, + DEFAULT_COLUMNS + [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}], + { + "visible": DEFAULT_COLUMNS, + "hidden": [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}], + }, + ), + # user has settings, organization has default settings - only selected columns are visible + ( + DEFAULT_COLUMNS[:3], + DEFAULT_COLUMNS, + {"visible": DEFAULT_COLUMNS[:3], "hidden": DEFAULT_COLUMNS[3:]}, + ), + # user has settings, organization has unchanged settings - only selected columns are visible + ( + DEFAULT_COLUMNS[:3] + + [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}], + DEFAULT_COLUMNS + [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}], + { + "visible": ( + DEFAULT_COLUMNS[:3] + + [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}] + ), + "hidden": DEFAULT_COLUMNS[3:], + }, + ), + # user has settings, organization has updated settings - column was removed, remove from settings + ( + DEFAULT_COLUMNS[:3] + + [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}], + DEFAULT_COLUMNS, + {"visible": DEFAULT_COLUMNS[:3], "hidden": DEFAULT_COLUMNS[3:]}, + ), + # user has settings with reordered columns, organization has unchanged settings - selected columns in particular + # order are visible + ( + [ + DEFAULT_COLUMNS[1], + DEFAULT_COLUMNS[3], + {"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}, + DEFAULT_COLUMNS[2], + ], + DEFAULT_COLUMNS + [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}], + { + "visible": [ + DEFAULT_COLUMNS[1], + DEFAULT_COLUMNS[3], + {"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}, + DEFAULT_COLUMNS[2], + ], + "hidden": DEFAULT_COLUMNS[:1] + DEFAULT_COLUMNS[4:], + }, + ), + ], +) +@pytest.mark.django_db +def test_alert_group_table_user_settings( + user_settings, + organization_settings, + expected_result, + make_organization_and_user, +): + organization, user = make_organization_and_user() + organization.update_alert_group_table_columns(organization_settings) + if user_settings: + user.update_alert_group_table_columns_settings(user_settings) + result = alert_group_table_user_settings(user) + assert result == expected_result + assert user.alert_groups_table_selected_columns == result["visible"] From 3d89fced934bed01e9b20ebaeb20e1e24fac3622 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 20 Nov 2023 15:43:40 +0200 Subject: [PATCH 38/67] show key values in the add modal --- .../ColumnsSelectorWrapper/ColumnsModal.tsx | 93 +++++++++++++++---- .../ColumnsSelectorWrapper.module.scss | 6 +- .../src/models/alertgroup/alertgroup.ts | 11 ++- .../src/pages/incidents/Incidents.tsx | 33 +++++-- grafana-plugin/src/utils/consts.ts | 4 +- 5 files changed, 115 insertions(+), 32 deletions(-) diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx index ddd533d0fb..ac523c08a8 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx @@ -1,6 +1,15 @@ import React, { useMemo, useState } from 'react'; -import { Button, Checkbox, HorizontalGroup, Input, LoadingPlaceholder, Modal, VerticalGroup } from '@grafana/ui'; +import { + Button, + Checkbox, + HorizontalGroup, + IconButton, + Input, + LoadingPlaceholder, + Modal, + VerticalGroup, +} from '@grafana/ui'; import cn from 'classnames/bind'; import { observer } from 'mobx-react'; @@ -8,13 +17,15 @@ import Text from 'components/Text/Text'; import styles from 'containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; import { AGColumn, AGColumnType } from 'models/alertgroup/alertgroup.types'; -import { Label } from 'models/label/label.types'; +import { Label, LabelValue } from 'models/label/label.types'; import { ActionKey } from 'models/loader/action-keys'; import LoaderStore from 'models/loader/loader'; import { useStore } from 'state/useStore'; import { openErrorNotification, pluralize } from 'utils'; import { UserActions } from 'utils/authorization'; import { useDebouncedCallback } from 'utils/hooks'; +import Block from 'components/GBlock/Block'; +import { LabelTag } from '@grafana/labels'; const cx = cn.bind(styles); @@ -27,6 +38,8 @@ interface ColumnsModalProps { interface SearchResult extends Label { isChecked: boolean; + isCollapsed: boolean; + values: any[]; } const DEBOUNCE_MS = 300; @@ -67,22 +80,39 @@ export const ColumnsModal: React.FC = observer( {inputRef?.current?.value && searchResults.length && ( {searchResults.map((result, index) => ( -
    - { - setSearchResults((items) => { - return items.map((item) => { - const updatedItem: SearchResult = { ...item, isChecked: !item.isChecked }; - return item.id === result.id ? updatedItem : item; + +
    + expandOrCollapseSearchResultItem(result, index)} + /> + + { + setSearchResults((items) => { + return items.map((item) => { + const updatedItem: SearchResult = { ...item, isChecked: !item.isChecked }; + return item.id === result.id ? updatedItem : item; + }); }); - }); - }} - /> - - {result.name} -
    + }} + /> + + {result.name} +
    + {!result.isCollapsed && ( + + {result.values === undefined ? ( + + ) : ( + renderLabelValues(result.name, result.values) + )} + + )} +
    ))}
    )} @@ -111,6 +141,33 @@ export const ColumnsModal: React.FC = observer(
    ); + function renderLabelValues(keyName: string, values: LabelValue[]) { + return ( + + {values.slice(0, 2).map((val) => ( + + ))} +
    {values.length > 2 ? `+ ${values.length - 2}` : ``}
    +
    + ); + } + + async function expandOrCollapseSearchResultItem(result: SearchResult, index: number) { + setSearchResults((items) => + items.map((it, idx) => (idx === index ? { ...it, isCollapsed: !it.isCollapsed } : it)) + ); + + await fetchLabelValues(result, index); + } + + async function fetchLabelValues(result: SearchResult, index: number) { + const labelResponse = await store.alertGroupStore.loadValuesForLabelKey(result.id); + + setSearchResults((items) => + items.map((it, idx) => (idx === index ? { ...it, values: labelResponse.values } : it)) + ); + } + function onCloseModal() { inputRef.current.value = ''; @@ -159,7 +216,7 @@ export const ColumnsModal: React.FC = observer( setSearchResults( availableKeysForSearching .filter((pair) => pair.name.indexOf(search) > -1) - .map((pair) => ({ ...pair, isChecked: false })) + .map((pair) => ({ ...pair, isChecked: false, isCollapsed: true, values: undefined })) ); } diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss index 499621e728..e1d1a2a383 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss @@ -17,4 +17,8 @@ .removal-modal { max-width: 500px; -} \ No newline at end of file +} + +.total-values-count { + margin-left: 16px; +} diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.ts b/grafana-plugin/src/models/alertgroup/alertgroup.ts index 24df3820b5..5901d8ef4e 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.ts @@ -3,7 +3,7 @@ import qs from 'query-string'; import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; import BaseStore from 'models/base_store'; -import { Label, LabelKey } from 'models/label/label.types'; +import { Label, LabelKey, LabelValue } from 'models/label/label.types'; import { ActionKey } from 'models/loader/action-keys'; import { User } from 'models/user/user.types'; import { makeRequest } from 'network'; @@ -476,16 +476,19 @@ export class AlertGroupStore extends BaseStore { } @action - public async loadValuesForLabelKey(key: LabelKey['id'], search = '') { + public async loadValuesForLabelKey( + key: LabelKey['id'], + search = '' + ): Promise<{ key: LabelKey; values: LabelValue[] }> { if (!key) { - return []; + return { key: undefined, values: [] }; } const result = await makeRequest(`/alertgroups/labels/id/${key}`, { params: { search }, }); - const filteredValues = result.values.filter((v) => v.name.toLowerCase().includes(search.toLowerCase())); // TODO remove after backend search implementation + const filteredValues = result.values.filter((v: LabelValue) => v.name.toLowerCase().includes(search.toLowerCase())); // TODO remove after backend search implementation return { ...result, values: filteredValues }; } diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index 74e38a5c2c..133f1cd78a 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -1,7 +1,7 @@ import React, { SyntheticEvent } from 'react'; import { LabelTag } from '@grafana/labels'; -import { Button, HorizontalGroup, Icon, VerticalGroup } from '@grafana/ui'; +import { Button, HorizontalGroup, Icon, RadioButtonGroup, VerticalGroup } from '@grafana/ui'; import cn from 'classnames/bind'; import { capitalize } from 'lodash-es'; import { observer } from 'mobx-react'; @@ -40,12 +40,13 @@ import { PageProps, WithStoreProps } from 'state/types'; import { withMobXProviderContext } from 'state/withStore'; import LocationHelper from 'utils/LocationHelper'; import { UserActions } from 'utils/authorization'; -import { PAGE, PLUGIN_ROOT, TEXT_ELLIPSIS_CLASS } from 'utils/consts'; +import { INCIDENT_HORIZONTAL_SCROLLING_STORAGE, PAGE, PLUGIN_ROOT, TEXT_ELLIPSIS_CLASS } from 'utils/consts'; import { TableColumn } from 'utils/types'; import styles from './Incidents.module.scss'; import { IncidentDropdown } from './parts/IncidentDropdown'; import { SilenceButtonCascader } from './parts/SilenceButtonCascader'; +import { getItem, setItem } from 'utils/localStorage'; const cx = cn.bind(styles); @@ -62,6 +63,7 @@ interface IncidentsPageState { pagination: Pagination; showAddAlertGroupForm: boolean; isSelectorColumnMenuOpen: boolean; + isHorizontalScrolling: boolean; } const POLLING_NUM_SECONDS = 15; @@ -96,6 +98,7 @@ class Incidents extends React.Component end: start + pageSize, }, isSelectorColumnMenuOpen: true, + isHorizontalScrolling: getItem(INCIDENT_HORIZONTAL_SCROLLING_STORAGE) || false, }; } @@ -350,6 +353,11 @@ class Incidents extends React.Component ); }; + onEnableHorizontalScroll = (value: boolean) => { + setItem(INCIDENT_HORIZONTAL_SCROLLING_STORAGE, value); + this.setState({ isHorizontalScrolling: value }); + }; + handleChangeItemsPerPage = (value: number) => { const { store } = this.props; @@ -372,7 +380,7 @@ class Incidents extends React.Component }; renderBulkActions = () => { - const { selectedIncidentIds, affectedRows } = this.state; + const { selectedIncidentIds, affectedRows, isHorizontalScrolling } = this.state; const { store } = this.props; if (!store.alertGroupStore.bulkActions) { @@ -453,6 +461,20 @@ class Incidents extends React.Component )} + {store.hasFeature(AppFeature.Labels) && ( + + )} + {store.hasFeature(AppFeature.Labels) && }
    @@ -461,7 +483,7 @@ class Incidents extends React.Component }; renderTable() { - const { selectedIncidentIds, pagination } = this.state; + const { selectedIncidentIds, pagination, isHorizontalScrolling } = this.state; const { alertGroupStore, filtersStore } = this.props.store; const { results, prev, next } = alertGroupStore.getAlertSearchResult('default'); @@ -503,7 +525,7 @@ class Incidents extends React.Component data={results} columns={this.getTableColumns()} tableLayout="auto" - scroll={{ x: true }} + scroll={{ x: isHorizontalScrolling ? '2000px' : true }} /> {this.shouldShowPagination() && (
    @@ -702,7 +724,6 @@ class Incidents extends React.Component const columnMapping: { [key: string]: TableColumn } = { ID: { - width: '5%', title: 'ID', key: 'id', render: this.renderId, diff --git a/grafana-plugin/src/utils/consts.ts b/grafana-plugin/src/utils/consts.ts index 9e3a86012d..9436fcd2d6 100644 --- a/grafana-plugin/src/utils/consts.ts +++ b/grafana-plugin/src/utils/consts.ts @@ -55,6 +55,4 @@ export enum PAGE { export const TEXT_ELLIPSIS_CLASS = 'overflow-child'; -export enum AGColumn { - Status = 'status', -} +export const INCIDENT_HORIZONTAL_SCROLLING_STORAGE = 'isIncidentalTableHorizontalScrolling'; \ No newline at end of file From 3a3d93848442edf891fe6817560d5448c258753c Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 20 Nov 2023 16:49:23 +0200 Subject: [PATCH 39/67] more tweaks --- .../ColumnsSelectorWrapper/ColumnsModal.tsx | 6 ++--- .../ColumnsSelectorWrapper.module.scss | 4 ++++ .../ColumnsSelectorWrapper.tsx | 22 ++++++++++++++----- .../src/models/loader/action-keys.ts | 1 + grafana-plugin/src/models/loader/loader.ts | 4 ++-- grafana-plugin/src/utils/decorators.ts | 14 +++++++++++- 6 files changed, 39 insertions(+), 12 deletions(-) diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx index ac523c08a8..7ddc1adc29 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx @@ -19,7 +19,7 @@ import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/W import { AGColumn, AGColumnType } from 'models/alertgroup/alertgroup.types'; import { Label, LabelValue } from 'models/label/label.types'; import { ActionKey } from 'models/loader/action-keys'; -import LoaderStore from 'models/loader/loader'; +import { LoaderStore } from 'models/loader/loader'; import { useStore } from 'state/useStore'; import { openErrorNotification, pluralize } from 'utils'; import { UserActions } from 'utils/authorization'; @@ -104,9 +104,9 @@ export const ColumnsModal: React.FC = observer( {result.name}
    {!result.isCollapsed && ( - + {result.values === undefined ? ( - + ) : ( renderLabelValues(result.name, result.values) )} diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss index e1d1a2a383..f0293f7e7b 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss @@ -22,3 +22,7 @@ .total-values-count { margin-left: 16px; } + +.values-block { + margin-bottom: 12px; +} \ No newline at end of file diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx index 4c832aa45f..d347d2974d 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState } from 'react'; -import { Button, HorizontalGroup, Icon, Modal, Toggletip, VerticalGroup } from '@grafana/ui'; +import { Button, HorizontalGroup, Icon, LoadingPlaceholder, Modal, Toggletip, VerticalGroup } from '@grafana/ui'; import cn from 'classnames/bind'; import Text from 'components/Text/Text'; @@ -11,12 +11,16 @@ import { Label } from 'models/label/label.types'; import { useStore } from 'state/useStore'; import { ColumnsModal } from './ColumnsModal'; +import { LoaderStore } from 'models/loader/loader'; +import { ActionKey } from 'models/loader/action-keys'; +import { WrapAutoLoadingState } from 'utils/decorators'; +import { observer } from 'mobx-react'; const cx = cn.bind(styles); interface ColumnsSelectorWrapperProps {} -const ColumnsSelectorWrapper: React.FC = () => { +const ColumnsSelectorWrapper: React.FC = observer(() => { const [isConfirmRemovalModalOpen, setIsConfirmRemovalModalOpen] = useState(false); const [columnToBeRemoved, setColumnToBeRemoved] = useState(undefined); const [isColumnAddModalOpen, setIsColumnAddModalOpen] = useState(false); @@ -35,6 +39,8 @@ const ColumnsSelectorWrapper: React.FC = () => { })(); }, [isColumnAddModalOpen]); + const isRemoveLoading = LoaderStore.isLoading(ActionKey.IS_REMOVING_COLUMN_FROM_ALERT_GROUP); + return ( <> = () => { className={cx('removal-modal')} > - Are you sure you want to remove column label {columnToBeRemoved?.name}? + Are you sure you want to remove column {columnToBeRemoved?.name}? - @@ -121,7 +131,7 @@ const ColumnsSelectorWrapper: React.FC = () => { // reset temporary cached columns alertGroupStore.temporaryColumns = [...alertGroupStore.columns]; } -}; +}); function forceOpenToggletip() { document.getElementById('toggletip-button')?.click(); diff --git a/grafana-plugin/src/models/loader/action-keys.ts b/grafana-plugin/src/models/loader/action-keys.ts index dcadf10b82..591c724afe 100644 --- a/grafana-plugin/src/models/loader/action-keys.ts +++ b/grafana-plugin/src/models/loader/action-keys.ts @@ -1,3 +1,4 @@ export enum ActionKey { IS_ADDING_NEW_COLUMN_TO_ALERT_GROUP = 'IS_ADDING_NEW_COLUMN_TO_ALERT_GROUP', + IS_REMOVING_COLUMN_FROM_ALERT_GROUP = 'IS_REMOVING_COLUMN_FROM_ALERT_GROUP', } diff --git a/grafana-plugin/src/models/loader/loader.ts b/grafana-plugin/src/models/loader/loader.ts index fd554798ca..1edcfbdb1b 100644 --- a/grafana-plugin/src/models/loader/loader.ts +++ b/grafana-plugin/src/models/loader/loader.ts @@ -4,7 +4,7 @@ interface LoadingResult { [key: string]: boolean; } -class LoaderStore { +class LoaderStoreClass { @observable items: LoadingResult = {}; @@ -17,4 +17,4 @@ class LoaderStore { } } -export default new LoaderStore() as LoaderStore; +export const LoaderStore = new LoaderStoreClass(); diff --git a/grafana-plugin/src/utils/decorators.ts b/grafana-plugin/src/utils/decorators.ts index 6b4a1892a5..90ba16b618 100644 --- a/grafana-plugin/src/utils/decorators.ts +++ b/grafana-plugin/src/utils/decorators.ts @@ -1,4 +1,4 @@ -import LoaderStore from 'models/loader/loader'; +import { LoaderStore } from 'models/loader/loader'; export function AutoLoadingState(actionKey: string) { return function (_target: object, _key: string, descriptor: PropertyDescriptor) { @@ -13,3 +13,15 @@ export function AutoLoadingState(actionKey: string) { }; }; } + +export function WrapAutoLoadingState(callback: Function, actionKey: string): (...params: any[]) => Promise { + return async (...params) => { + LoaderStore.setLoadingAction(actionKey, true); + + try { + await callback(...params); + } finally { + LoaderStore.setLoadingAction(actionKey, false); + } + }; +} From 737f1a900ceb9f9fe8d42d255895fcf3f57a12a8 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Tue, 21 Nov 2023 11:32:01 +0200 Subject: [PATCH 40/67] admin permission + table calculation horizontal scrolling --- .../ColumnsSelectorWrapper.tsx | 18 +++++++++++------- .../src/pages/incidents/Incidents.tsx | 6 ++++-- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx index d347d2974d..ef740322e4 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx @@ -15,6 +15,8 @@ import { LoaderStore } from 'models/loader/loader'; import { ActionKey } from 'models/loader/action-keys'; import { WrapAutoLoadingState } from 'utils/decorators'; import { observer } from 'mobx-react'; +import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; +import { UserActions } from 'utils/authorization'; const cx = cn.bind(styles); @@ -64,13 +66,15 @@ const ColumnsSelectorWrapper: React.FC = observer(( - + + + diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index 133f1cd78a..0b8d548339 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -510,6 +510,8 @@ class Incidents extends React.Component ); } + const tableColumns = this.getTableColumns(); + return (
    {this.renderBulkActions()} @@ -523,9 +525,9 @@ class Incidents extends React.Component }} rowKey="pk" data={results} - columns={this.getTableColumns()} + columns={tableColumns} tableLayout="auto" - scroll={{ x: isHorizontalScrolling ? '2000px' : true }} + scroll={{ x: isHorizontalScrolling ? `${Math.max(2000, tableColumns.length * 200)}px` : true }} /> {this.shouldShowPagination() && (
    From 9714fdefaffc741db3ea45e87bf65be703f2709d Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 21 Nov 2023 11:53:15 +0100 Subject: [PATCH 41/67] Check labels feature flag for alert group columns settings endpoints --- engine/apps/api/views/alert_group_table_settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/engine/apps/api/views/alert_group_table_settings.py b/engine/apps/api/views/alert_group_table_settings.py index f3b8c15899..8c62bde5cf 100644 --- a/engine/apps/api/views/alert_group_table_settings.py +++ b/engine/apps/api/views/alert_group_table_settings.py @@ -2,16 +2,16 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from rest_framework.viewsets import ViewSet from apps.api.permissions import RBACPermission from apps.api.serializers.alert_group_table_settings import AlertGroupTableColumnsListSerializer +from apps.api.views.labels import LabelsFeatureFlagViewSet from apps.auth_token.auth import PluginAuthentication from apps.user_management.constants import AlertGroupTableColumn from apps.user_management.utils import alert_group_table_user_settings -class AlertGroupTableColumnsViewSet(ViewSet): +class AlertGroupTableColumnsViewSet(LabelsFeatureFlagViewSet): authentication_classes = (PluginAuthentication,) permission_classes = (IsAuthenticated, RBACPermission) From 1f3bc0c9a14025cc2e8a127a238b8f4fe58206b1 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 21 Nov 2023 13:06:59 +0100 Subject: [PATCH 42/67] Fix migration lint --- .../apps/user_management/migrations/0018_auto_20231115_1206.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/engine/apps/user_management/migrations/0018_auto_20231115_1206.py b/engine/apps/user_management/migrations/0018_auto_20231115_1206.py index 4d6b29b77b..a4d95a276c 100644 --- a/engine/apps/user_management/migrations/0018_auto_20231115_1206.py +++ b/engine/apps/user_management/migrations/0018_auto_20231115_1206.py @@ -3,6 +3,8 @@ import apps.user_management.utils from django.db import migrations, models +import django_migration_linter as linter + class Migration(migrations.Migration): @@ -11,6 +13,7 @@ class Migration(migrations.Migration): ] operations = [ + linter.IgnoreMigration(), migrations.AddField( model_name='organization', name='alert_group_table_columns', From 1e7588497f3984b4fa6dec3d6af422ac37e62406 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Tue, 21 Nov 2023 16:37:15 +0200 Subject: [PATCH 43/67] updates on hard reset --- .../ColumnsSelector/ColumnsSelector.tsx | 65 ++++++++++++------- .../ColumnsSelectorWrapper/ColumnsModal.tsx | 4 +- .../ColumnsSelectorWrapper.tsx | 21 ++---- .../src/models/alertgroup/alertgroup.ts | 3 - .../src/models/loader/action-keys.ts | 1 + .../src/pages/incidents/Incidents.tsx | 6 +- grafana-plugin/src/utils/consts.ts | 2 +- 7 files changed, 57 insertions(+), 45 deletions(-) diff --git a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx index 4659142d1c..5d9f0eb448 100644 --- a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx +++ b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useRef } from 'react'; +import React, { useMemo, useRef } from 'react'; import { DndContext, @@ -17,9 +17,9 @@ import { useSortable, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; -import { Button, Checkbox, Icon, IconButton, Tooltip } from '@grafana/ui'; +import { Button, Checkbox, Icon, IconButton, LoadingPlaceholder, Tooltip } from '@grafana/ui'; import cn from 'classnames/bind'; -import { cloneDeep, isEqual } from 'lodash-es'; +import { isEqual } from 'lodash-es'; import { observer } from 'mobx-react'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; @@ -27,9 +27,12 @@ import Text from 'components/Text/Text'; import styles from 'containers/ColumnsSelector/ColumnsSelector.module.scss'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; import { AGColumn, AGColumnType } from 'models/alertgroup/alertgroup.types'; +import { ActionKey } from 'models/loader/action-keys'; +import { LoaderStore } from 'models/loader/loader'; import { useStore } from 'state/useStore'; import { openErrorNotification } from 'utils'; import { UserActions } from 'utils/authorization'; +import { WrapAutoLoadingState } from 'utils/decorators'; const cx = cn.bind(styles); const TRANSITION_MS = 500; @@ -101,21 +104,12 @@ interface ColumnsSelectorProps { export const ColumnsSelector: React.FC = observer( ({ onColumnAddModalOpen, onConfirmRemovalModalOpen }) => { const { alertGroupStore } = useStore(); - const { columns: items, temporaryColumns } = alertGroupStore; + const { columns } = alertGroupStore; - const visibleColumns = items.filter((col) => col.isVisible); - const hiddenColumns = items.filter((col) => !col.isVisible); + const visibleColumns = columns.filter((col) => col.isVisible); + const hiddenColumns = columns.filter((col) => !col.isVisible); - useEffect(() => { - if (!temporaryColumns.length) { - alertGroupStore.temporaryColumns = cloneDeep(items); - } - }, []); - - const canResetData = useMemo( - () => !isEqual(temporaryColumns, items), - [alertGroupStore.columns, alertGroupStore.temporaryColumns] - ); + const canResetData = useMemo(() => !isEqual(columns, getDefaultData()), [alertGroupStore.columns]); const sensors = useSensors( useSensor(PointerSensor), @@ -124,6 +118,8 @@ export const ColumnsSelector: React.FC = observer( }) ); + const isResetLoading = LoaderStore.isLoading(ActionKey.IS_RESETING_COLUMNS_FROM_ALERT_GROUP); + return (
    @@ -136,7 +132,7 @@ export const ColumnsSelector: React.FC = observer( handleDragEnd(ev, true)}> - + {visibleColumns.map((column) => ( @@ -159,7 +155,7 @@ export const ColumnsSelector: React.FC = observer( handleDragEnd(ev, false)}> - + {hiddenColumns.map((column) => ( @@ -177,11 +173,15 @@ export const ColumnsSelector: React.FC = observer(
    - - @@ -189,8 +189,27 @@ export const ColumnsSelector: React.FC = observer(
    ); - function onReset() { - alertGroupStore.columns = [...alertGroupStore.temporaryColumns]; + async function onReset() { + const columnsDefaultValues = getDefaultData(); + + return alertGroupStore + .updateTableSettings(columnsDefaultValues, true) + .then(() => alertGroupStore.fetchTableSettings()); + } + + function getDefaultData() { + const { columns } = alertGroupStore; + + const columnsDefaultValues: { visible: AGColumn[]; hidden: AGColumn[] } = { + visible: columns + .filter((col) => col.type === AGColumnType.DEFAULT) + .sort((a, b) => (a.id as number) - (b.id as number)), + hidden: columns + .filter((col) => col.type === AGColumnType.LABEL) + .sort((a, b) => a.id.toString().localeCompare(b.id.toString())), + }; + + return columnsDefaultValues; } async function onItemChange(id: string | number) { diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx index 7ddc1adc29..6ae63aabc2 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx @@ -1,5 +1,6 @@ import React, { useMemo, useState } from 'react'; +import { LabelTag } from '@grafana/labels'; import { Button, Checkbox, @@ -13,6 +14,7 @@ import { import cn from 'classnames/bind'; import { observer } from 'mobx-react'; +import Block from 'components/GBlock/Block'; import Text from 'components/Text/Text'; import styles from 'containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; @@ -24,8 +26,6 @@ import { useStore } from 'state/useStore'; import { openErrorNotification, pluralize } from 'utils'; import { UserActions } from 'utils/authorization'; import { useDebouncedCallback } from 'utils/hooks'; -import Block from 'components/GBlock/Block'; -import { LabelTag } from '@grafana/labels'; const cx = cn.bind(styles); diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx index ef740322e4..5bce87284d 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx @@ -2,21 +2,21 @@ import React, { useEffect, useRef, useState } from 'react'; import { Button, HorizontalGroup, Icon, LoadingPlaceholder, Modal, Toggletip, VerticalGroup } from '@grafana/ui'; import cn from 'classnames/bind'; +import { observer } from 'mobx-react'; import Text from 'components/Text/Text'; import { ColumnsSelector, convertColumnsToTableSettings } from 'containers/ColumnsSelector/ColumnsSelector'; import styles from 'containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss'; +import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; import { AGColumn } from 'models/alertgroup/alertgroup.types'; import { Label } from 'models/label/label.types'; +import { ActionKey } from 'models/loader/action-keys'; +import { LoaderStore } from 'models/loader/loader'; import { useStore } from 'state/useStore'; +import { UserActions } from 'utils/authorization'; +import { WrapAutoLoadingState } from 'utils/decorators'; import { ColumnsModal } from './ColumnsModal'; -import { LoaderStore } from 'models/loader/loader'; -import { ActionKey } from 'models/loader/action-keys'; -import { WrapAutoLoadingState } from 'utils/decorators'; -import { observer } from 'mobx-react'; -import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; -import { UserActions } from 'utils/authorization'; const cx = cn.bind(styles); @@ -91,9 +91,7 @@ const ColumnsSelectorWrapper: React.FC = observer(( /> } placement={'bottom-end'} - show={true} closeButton={false} - onClose={onToggletipClose} > {renderToggletipButton()} @@ -128,13 +126,6 @@ const ColumnsSelectorWrapper: React.FC = observer(( ); } - - function onToggletipClose() { - const { alertGroupStore } = store; - - // reset temporary cached columns - alertGroupStore.temporaryColumns = [...alertGroupStore.columns]; - } }); function forceOpenToggletip() { diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.ts b/grafana-plugin/src/models/alertgroup/alertgroup.ts index 5901d8ef4e..a847108edd 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.ts @@ -77,9 +77,6 @@ export class AlertGroupStore extends BaseStore { @observable columns: AGColumn[] = []; - @observable - temporaryColumns: AGColumn[] = []; - constructor(rootStore: RootStore) { super(rootStore); diff --git a/grafana-plugin/src/models/loader/action-keys.ts b/grafana-plugin/src/models/loader/action-keys.ts index 591c724afe..a03903c788 100644 --- a/grafana-plugin/src/models/loader/action-keys.ts +++ b/grafana-plugin/src/models/loader/action-keys.ts @@ -1,4 +1,5 @@ export enum ActionKey { IS_ADDING_NEW_COLUMN_TO_ALERT_GROUP = 'IS_ADDING_NEW_COLUMN_TO_ALERT_GROUP', IS_REMOVING_COLUMN_FROM_ALERT_GROUP = 'IS_REMOVING_COLUMN_FROM_ALERT_GROUP', + IS_RESETING_COLUMNS_FROM_ALERT_GROUP = 'IS_RESETING_COLUMNS_FROM_ALERT_GROUP', } diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index 0b8d548339..99d6aec8ec 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -41,12 +41,12 @@ import { withMobXProviderContext } from 'state/withStore'; import LocationHelper from 'utils/LocationHelper'; import { UserActions } from 'utils/authorization'; import { INCIDENT_HORIZONTAL_SCROLLING_STORAGE, PAGE, PLUGIN_ROOT, TEXT_ELLIPSIS_CLASS } from 'utils/consts'; +import { getItem, setItem } from 'utils/localStorage'; import { TableColumn } from 'utils/types'; import styles from './Incidents.module.scss'; import { IncidentDropdown } from './parts/IncidentDropdown'; import { SilenceButtonCascader } from './parts/SilenceButtonCascader'; -import { getItem, setItem } from 'utils/localStorage'; const cx = cn.bind(styles); @@ -772,6 +772,10 @@ class Incidents extends React.Component }, }; + if (!store.hasFeature(AppFeature.Labels)) { + return Object.keys(columnMapping).map((col) => columnMapping[col]); + } + const mappedColumns: TableColumn[] = store.alertGroupStore.columns .filter((col) => col.isVisible) .map((column: AGColumn): TableColumn => { diff --git a/grafana-plugin/src/utils/consts.ts b/grafana-plugin/src/utils/consts.ts index 9436fcd2d6..1857c08a80 100644 --- a/grafana-plugin/src/utils/consts.ts +++ b/grafana-plugin/src/utils/consts.ts @@ -55,4 +55,4 @@ export enum PAGE { export const TEXT_ELLIPSIS_CLASS = 'overflow-child'; -export const INCIDENT_HORIZONTAL_SCROLLING_STORAGE = 'isIncidentalTableHorizontalScrolling'; \ No newline at end of file +export const INCIDENT_HORIZONTAL_SCROLLING_STORAGE = 'isIncidentalTableHorizontalScrolling'; From f7b262d372a7952279182d7c5113d2eb0c9384ea Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Tue, 21 Nov 2023 16:40:29 +0200 Subject: [PATCH 44/67] sorting + labels check --- .../src/containers/ColumnsSelector/ColumnsSelector.tsx | 4 +++- grafana-plugin/src/pages/incidents/Incidents.tsx | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx index 5d9f0eb448..72f355ad94 100644 --- a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx +++ b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx @@ -107,7 +107,9 @@ export const ColumnsSelector: React.FC = observer( const { columns } = alertGroupStore; const visibleColumns = columns.filter((col) => col.isVisible); - const hiddenColumns = columns.filter((col) => !col.isVisible); + const hiddenColumns = columns + .filter((col) => !col.isVisible) + .sort((a, b) => a.id.toString().localeCompare(b.id.toString())); const canResetData = useMemo(() => !isEqual(columns, getDefaultData()), [alertGroupStore.columns]); diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index 99d6aec8ec..4088ea4c29 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -105,11 +105,15 @@ class Incidents extends React.Component private pollingIntervalId: NodeJS.Timer = undefined; componentDidMount() { - const { alertGroupStore } = this.props.store; + const { store } = this.props; + const { alertGroupStore } = store; alertGroupStore.updateBulkActions(); alertGroupStore.updateSilenceOptions(); - alertGroupStore.fetchTableSettings(); + + if (store.hasFeature(AppFeature.Labels)) { + alertGroupStore.fetchTableSettings(); + } } componentWillUnmount(): void { From 67d4d374cfab2224677badf3ef2a00b948141db8 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Wed, 22 Nov 2023 13:58:49 +0200 Subject: [PATCH 45/67] emotionjs usages, tweaks, changed Floating Menu display component --- .../src/components/GTable/GTable.tsx | 2 +- .../ColumnsSelector.module.scss | 95 ------------------- .../ColumnsSelector/ColumnsSelector.styles.ts | 93 ++++++++++++++++++ .../ColumnsSelector/ColumnsSelector.tsx | 60 +++++++----- .../ColumnsSelectorWrapper/ColumnsModal.tsx | 19 ++-- .../ColumnsSelectorWrapper.module.scss | 28 ------ .../ColumnsSelectorWrapper.styles.ts | 46 +++++++++ .../ColumnsSelectorWrapper.tsx | 80 ++++++++++------ .../src/pages/incidents/Incidents.tsx | 17 +++- 9 files changed, 253 insertions(+), 187 deletions(-) delete mode 100644 grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.module.scss create mode 100644 grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.styles.ts delete mode 100644 grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss create mode 100644 grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.styles.ts diff --git a/grafana-plugin/src/components/GTable/GTable.tsx b/grafana-plugin/src/components/GTable/GTable.tsx index 5251e4e37f..a646c16b93 100644 --- a/grafana-plugin/src/components/GTable/GTable.tsx +++ b/grafana-plugin/src/components/GTable/GTable.tsx @@ -115,7 +115,7 @@ const GTable = (props: Props { + return { + columnsSelectorView: css` + min-width: 230px; + `, + + columnsVisibleSection: css` + margin-bottom: 16px; + `, + + columnsHeader: css` + display: block !important; + margin-bottom: 16px; + `, + columnsHeaderSmall: css` + display: block !important; + margin-bottom: 8px; + `, + + columnsHeaderSecondary: css` + display: block; + margin-bottom: 8px; + `, + + columnsHiddenSection: css` + margin-bottom: 20px; + max-height: 250px; + overflow-y: auto; + `, + columnsSelectorButtons: css` + display: flex; + justify-content: flex-end; + gap: 8px; + width: 100%; + `, + + columnItem: css` + gap: 12px; + display: flex; + padding-left: 25px; + + &:hover .columns-icon-trash { + display: block; + } + `, + + columnsCheckbox: css` + position: absolute; + top: 2px; + left: 0; + `, + + columnsIcon: css` + display: block; + margin-left: auto; + position: relative; + top: -2px; + + &::before { + top: 50%; + left: 50%; + border-radius: 50%; + transform: translate(-50%, -60%); + } + `, + + columnsIconTrash: css` + display: none; + `, + + columnRow: css` + position: relative; + margin-bottom: 6px; + height: 22px; + `, + + columnName: css` + text-wrap: nowrap; + max-width: 180px; + text-overflow: ellipsis; + display: block; + overflow: hidden; + `, + + labelIcon: css` + margin: 0 !important; + padding: 0 !important; + `, + }; +}; diff --git a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx index 72f355ad94..6dd6d1fd41 100644 --- a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx +++ b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx @@ -17,14 +17,12 @@ import { useSortable, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; -import { Button, Checkbox, Icon, IconButton, LoadingPlaceholder, Tooltip } from '@grafana/ui'; -import cn from 'classnames/bind'; +import { Button, Checkbox, Icon, IconButton, LoadingPlaceholder, Tooltip, useStyles2 } from '@grafana/ui'; import { isEqual } from 'lodash-es'; import { observer } from 'mobx-react'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; import Text from 'components/Text/Text'; -import styles from 'containers/ColumnsSelector/ColumnsSelector.module.scss'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; import { AGColumn, AGColumnType } from 'models/alertgroup/alertgroup.types'; import { ActionKey } from 'models/loader/action-keys'; @@ -34,7 +32,8 @@ import { openErrorNotification } from 'utils'; import { UserActions } from 'utils/authorization'; import { WrapAutoLoadingState } from 'utils/decorators'; -const cx = cn.bind(styles); +import { getColumnsSelectorStyles } from './ColumnsSelector.styles'; + const TRANSITION_MS = 500; interface ColumnRowProps { @@ -46,6 +45,8 @@ interface ColumnRowProps { const ColumnRow: React.FC = ({ column, onItemChange, onColumnRemoval }) => { const dnd = useSortable({ id: column.id }); + const styles = useStyles2(getColumnsSelectorStyles); + const { attributes, listeners, setNodeRef, transform, transition } = dnd; const columnElRef = useRef(undefined); @@ -55,13 +56,13 @@ const ColumnRow: React.FC = ({ column, onItemChange, onColumnRem }; return ( -
    -
    - {column.name} +
    +
    + {column.name} {column.type === AGColumnType.LABEL && ( - + )} @@ -69,14 +70,14 @@ const ColumnRow: React.FC = ({ column, onItemChange, onColumnRem ) : column.type === AGColumnType.LABEL ? ( = ({ column, onItemChange, onColumnRem
    onItemChange(column.id)} @@ -104,6 +105,9 @@ interface ColumnsSelectorProps { export const ColumnsSelector: React.FC = observer( ({ onColumnAddModalOpen, onConfirmRemovalModalOpen }) => { const { alertGroupStore } = useStore(); + + const styles = useStyles2(getColumnsSelectorStyles); + const { columns } = alertGroupStore; const visibleColumns = columns.filter((col) => col.isVisible); @@ -123,17 +127,22 @@ export const ColumnsSelector: React.FC = observer( const isResetLoading = LoaderStore.isLoading(ActionKey.IS_RESETING_COLUMNS_FROM_ALERT_GROUP); return ( -
    - - Fields Settings +
    + + Columns Settings -
    - +
    + Visible ({visibleColumns.length}) - handleDragEnd(ev, true)}> + handleDragEnd(ev, true)} + > {visibleColumns.map((column) => ( @@ -151,12 +160,17 @@ export const ColumnsSelector: React.FC = observer(
    -
    - +
    + Hidden ({hiddenColumns.length}) - handleDragEnd(ev, false)}> + handleDragEnd(ev, false)} + > {hiddenColumns.map((column) => ( @@ -174,9 +188,11 @@ export const ColumnsSelector: React.FC = observer(
    -
    +
    diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx index 6ae63aabc2..d519a175ca 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx @@ -10,13 +10,12 @@ import { LoadingPlaceholder, Modal, VerticalGroup, + useStyles2, } from '@grafana/ui'; -import cn from 'classnames/bind'; import { observer } from 'mobx-react'; import Block from 'components/GBlock/Block'; import Text from 'components/Text/Text'; -import styles from 'containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; import { AGColumn, AGColumnType } from 'models/alertgroup/alertgroup.types'; import { Label, LabelValue } from 'models/label/label.types'; @@ -27,7 +26,7 @@ import { openErrorNotification, pluralize } from 'utils'; import { UserActions } from 'utils/authorization'; import { useDebouncedCallback } from 'utils/hooks'; -const cx = cn.bind(styles); +import { getColumnsSelectorWrapperStyles } from './ColumnsSelectorWrapper.styles'; interface ColumnsModalProps { isModalOpen: boolean; @@ -47,6 +46,8 @@ const DEBOUNCE_MS = 300; export const ColumnsModal: React.FC = observer( ({ isModalOpen, labelKeys, setIsModalOpen, inputRef }) => { const store = useStore(); + const styles = useStyles2(getColumnsSelectorWrapperStyles); + const [searchResults, setSearchResults] = useState([]); const debouncedOnInputChange = useDebouncedCallback(onInputChange, DEBOUNCE_MS); @@ -60,10 +61,10 @@ export const ColumnsModal: React.FC = observer( return ( -
    +
    = observer( {searchResults.map((result, index) => ( -
    +
    expandOrCollapseSearchResultItem(result, index)} /> @@ -104,7 +105,7 @@ export const ColumnsModal: React.FC = observer( {result.name}
    {!result.isCollapsed && ( - + {result.values === undefined ? ( ) : ( @@ -147,7 +148,7 @@ export const ColumnsModal: React.FC = observer( {values.slice(0, 2).map((val) => ( ))} -
    {values.length > 2 ? `+ ${values.length - 2}` : ``}
    +
    {values.length > 2 ? `+ ${values.length - 2}` : ``}
    ); } diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss deleted file mode 100644 index f0293f7e7b..0000000000 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss +++ /dev/null @@ -1,28 +0,0 @@ -.input { - margin-bottom: 16px; -} - -.field-row { - width: 100%; - display: flex; - flex-direction: row; - align-items: center; - gap: 16px; -} - -.content { - width: 100%; - min-height: 100px; -} - -.removal-modal { - max-width: 500px; -} - -.total-values-count { - margin-left: 16px; -} - -.values-block { - margin-bottom: 12px; -} \ No newline at end of file diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.styles.ts b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.styles.ts new file mode 100644 index 0000000000..14f30389e9 --- /dev/null +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.styles.ts @@ -0,0 +1,46 @@ +import { GrafanaTheme2 } from '@grafana/data'; +import { css } from '@emotion/css'; + +export const getColumnsSelectorWrapperStyles = (theme: GrafanaTheme2) => { + return { + input: css` + margin-bottom: 16px; + `, + fieldRow: css` + width: 100%; + display: flex; + flex-direction: row; + align-items: 'center'; + gap: 16px; + `, + content: css` + width: 100%; + min-height: 100px; + `, + removalModal: css` + max-width: 500px; + `, + totalValuesCount: css` + margin-left: 16px; + `, + valuesBlock: css` + margin-bottom: 12px; + `, + floatingContainer: css` + position: relative; + `, + floatingContent: css` + position: absolute; + top: 40px; + right: 0; + display: none; + background-color: ${theme.colors.background.secondary}; + padding: 16px; + z-index: 101; + overflow: hidden; + `, + floatingContentVisible: css` + display: block; + `, + }; +}; diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx index 5bce87284d..1502c53201 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx @@ -1,12 +1,12 @@ import React, { useEffect, useRef, useState } from 'react'; -import { Button, HorizontalGroup, Icon, LoadingPlaceholder, Modal, Toggletip, VerticalGroup } from '@grafana/ui'; -import cn from 'classnames/bind'; +import { Button, HorizontalGroup, Icon, LoadingPlaceholder, Modal, VerticalGroup } from '@grafana/ui'; +import { useStyles2 } from '@grafana/ui'; import { observer } from 'mobx-react'; import Text from 'components/Text/Text'; import { ColumnsSelector, convertColumnsToTableSettings } from 'containers/ColumnsSelector/ColumnsSelector'; -import styles from 'containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.module.scss'; +import { getColumnsSelectorWrapperStyles } from 'containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.styles'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; import { AGColumn } from 'models/alertgroup/alertgroup.types'; import { Label } from 'models/label/label.types'; @@ -18,18 +18,20 @@ import { WrapAutoLoadingState } from 'utils/decorators'; import { ColumnsModal } from './ColumnsModal'; -const cx = cn.bind(styles); - interface ColumnsSelectorWrapperProps {} const ColumnsSelectorWrapper: React.FC = observer(() => { const [isConfirmRemovalModalOpen, setIsConfirmRemovalModalOpen] = useState(false); const [columnToBeRemoved, setColumnToBeRemoved] = useState(undefined); const [isColumnAddModalOpen, setIsColumnAddModalOpen] = useState(false); + const [isFloatingDisplayOpen, setIsFloatingDisplayOpen] = useState(false); const [labelKeys, setLabelKeys] = useState([]); const inputRef = useRef(null); + const wrappingFloatingContainerRef = useRef(null); + + const styles = useStyles2(getColumnsSelectorWrapperStyles); const store = useStore(); @@ -41,6 +43,14 @@ const ColumnsSelectorWrapper: React.FC = observer(( })(); }, [isColumnAddModalOpen]); + useEffect(() => { + document.addEventListener('click', onFloatingDisplayClick); + + return () => { + document.removeEventListener('click', onFloatingDisplayClick); + }; + }, []); + const isRemoveLoading = LoaderStore.isLoading(ActionKey.IS_REMOVING_COLUMN_FROM_ALERT_GROUP); return ( @@ -57,7 +67,7 @@ const ColumnsSelectorWrapper: React.FC = observer(( isOpen={isConfirmRemovalModalOpen} title={'Remove column'} onDismiss={onConfirmRemovalClose} - className={cx('removal-modal')} + className={styles.removalModal} > Are you sure you want to remove column {columnToBeRemoved?.name}? @@ -79,28 +89,38 @@ const ColumnsSelectorWrapper: React.FC = observer(( - {!isColumnAddModalOpen && !isConfirmRemovalModalOpen ? ( - setIsColumnAddModalOpen(!isColumnAddModalOpen)} - onConfirmRemovalModalOpen={(column: AGColumn) => { - setIsConfirmRemovalModalOpen(!isConfirmRemovalModalOpen); - setColumnToBeRemoved(column); - }} - /> - } - placement={'bottom-end'} - closeButton={false} - > - {renderToggletipButton()} - - ) : ( - renderToggletipButton() - )} +
    + {!isColumnAddModalOpen && !isConfirmRemovalModalOpen ? ( +
    + {renderToggletipButton()} +
    + setIsColumnAddModalOpen(!isColumnAddModalOpen)} + onConfirmRemovalModalOpen={(column: AGColumn) => { + setIsConfirmRemovalModalOpen(!isConfirmRemovalModalOpen); + setColumnToBeRemoved(column); + }} + /> +
    +
    + ) : ( + renderToggletipButton() + )} +
    ); + function onFloatingDisplayClick(event) { + const element = wrappingFloatingContainerRef.current; + const isInside = element?.contains(event.target as HTMLDivElement); + + if (!isInside) { + setIsFloatingDisplayOpen(false); + } + } + function onConfirmRemovalClose(): void { setIsConfirmRemovalModalOpen(false); forceOpenToggletip(); @@ -118,9 +138,15 @@ const ColumnsSelectorWrapper: React.FC = observer(( function renderToggletipButton() { return ( - diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index 4088ea4c29..c2f6e2b3ca 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -727,15 +727,18 @@ class Incidents extends React.Component getTableColumns(): TableColumn[] { const { store } = this.props; + const { isHorizontalScrolling } = this.state; const columnMapping: { [key: string]: TableColumn } = { ID: { + width: isHorizontalScrolling ? '100px' : undefined, title: 'ID', key: 'id', render: this.renderId, }, Status: { title: 'Status', + width: isHorizontalScrolling ? '140px' : undefined, key: 'time', render: this.renderStatus, }, @@ -769,14 +772,18 @@ class Incidents extends React.Component key: 'users', render: renderRelatedUsers, }, - Labels: { + }; + + if (store.hasFeature(AppFeature.Labels)) { + // add labels specific column if enabled + columnMapping['Labels'] = { + width: '60px', title: 'Labels', key: 'labels', render: this.renderLabels, - }, - }; - - if (!store.hasFeature(AppFeature.Labels)) { + }; + } else { + // no filtering needed if we don't have Labels enabled return Object.keys(columnMapping).map((col) => columnMapping[col]); } From 36cc70965b1a9958e0805ed6da6a554b53d5c5ee Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Wed, 22 Nov 2023 14:03:51 +0200 Subject: [PATCH 46/67] linter --- .../src/containers/ColumnsSelector/ColumnsSelector.styles.ts | 2 +- .../src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx | 4 ++-- .../ColumnsSelectorWrapper/ColumnsSelectorWrapper.styles.ts | 2 +- .../ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx | 3 +-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.styles.ts b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.styles.ts index 4c853517d0..50593f1a5a 100644 --- a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.styles.ts +++ b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.styles.ts @@ -1,5 +1,5 @@ -import { GrafanaTheme2 } from '@grafana/data'; import { css } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data'; export const getColumnsSelectorStyles = (_theme: GrafanaTheme2) => { return { diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx index d519a175ca..04caf5d453 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx @@ -81,8 +81,8 @@ export const ColumnsModal: React.FC = observer( {inputRef?.current?.value && searchResults.length && ( {searchResults.map((result, index) => ( - -
    + +
    { return { diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx index 1502c53201..c16948f400 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useRef, useState } from 'react'; -import { Button, HorizontalGroup, Icon, LoadingPlaceholder, Modal, VerticalGroup } from '@grafana/ui'; -import { useStyles2 } from '@grafana/ui'; +import { useStyles2, Button, HorizontalGroup, Icon, LoadingPlaceholder, Modal, VerticalGroup } from '@grafana/ui'; import { observer } from 'mobx-react'; import Text from 'components/Text/Text'; From f369a750e69fd4aaec8e9fc6e8f35ec83a9f3a7d Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Wed, 22 Nov 2023 14:21:00 +0200 Subject: [PATCH 47/67] table cols sizing --- grafana-plugin/src/pages/incidents/Incidents.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index c2f6e2b3ca..2768720e34 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -731,46 +731,52 @@ class Incidents extends React.Component const columnMapping: { [key: string]: TableColumn } = { ID: { - width: isHorizontalScrolling ? '100px' : undefined, title: 'ID', key: 'id', render: this.renderId, + width: isHorizontalScrolling ? '100px' : '10%', }, Status: { title: 'Status', - width: isHorizontalScrolling ? '140px' : undefined, key: 'time', render: this.renderStatus, + width: '140px', }, Alerts: { title: 'Alerts', key: 'alerts', render: this.renderAlertsCounter, + width: '100px', }, Integration: { title: 'Integration', key: 'integration', render: this.renderSource, + width: isHorizontalScrolling ? undefined : '15%', }, Title: { title: 'Title', key: 'title', render: this.renderTitle, + width: isHorizontalScrolling ? undefined : '35%', }, Created: { title: 'Created', key: 'created', render: this.renderStartedAt, + width: isHorizontalScrolling ? undefined : '10%', }, Team: { title: 'Team', key: 'team', render: (item: AlertType) => this.renderTeam(item, store.grafanaTeamStore.items), + width: isHorizontalScrolling ? undefined : '10%', }, Users: { title: 'Users', key: 'users', render: renderRelatedUsers, + width: isHorizontalScrolling ? undefined : '15%', }, }; From 7c3c3507219a2f87166d304382b184cfec1f4ce3 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 23 Nov 2023 10:23:09 +0100 Subject: [PATCH 48/67] Update typing --- engine/apps/api/views/alert_group_table_settings.py | 7 ++++--- engine/apps/user_management/models/organization.py | 3 ++- engine/apps/user_management/models/user.py | 3 ++- engine/apps/user_management/utils.py | 7 +++++-- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/engine/apps/api/views/alert_group_table_settings.py b/engine/apps/api/views/alert_group_table_settings.py index 8c62bde5cf..48ecc0d933 100644 --- a/engine/apps/api/views/alert_group_table_settings.py +++ b/engine/apps/api/views/alert_group_table_settings.py @@ -1,6 +1,7 @@ import typing from rest_framework.permissions import IsAuthenticated +from rest_framework.request import Request from rest_framework.response import Response from apps.api.permissions import RBACPermission @@ -21,11 +22,11 @@ class AlertGroupTableColumnsViewSet(LabelsFeatureFlagViewSet): "update_columns_list": [RBACPermission.Permissions.OTHER_SETTINGS_WRITE], } - def get_columns(self, request): + def get_columns(self, request: Request) -> Response: user = request.user return Response(alert_group_table_user_settings(user)) - def update_columns_list(self, request): + def update_columns_list(self, request: Request) -> Response: """add/remove columns for organization""" user = request.user organization = request.auth.organization @@ -39,7 +40,7 @@ def update_columns_list(self, request): organization.update_alert_group_table_columns(columns) return Response(alert_group_table_user_settings(user)) - def update_columns_settings(self, request): + def update_columns_settings(self, request: Request) -> Response: """select/hide/change order for user""" user = request.user serializer = AlertGroupTableColumnsListSerializer( diff --git a/engine/apps/user_management/models/organization.py b/engine/apps/user_management/models/organization.py index 30e3efebde..c84ba80b9c 100644 --- a/engine/apps/user_management/models/organization.py +++ b/engine/apps/user_management/models/organization.py @@ -11,6 +11,7 @@ from mirage import fields as mirage_fields from apps.alerts.models import MaintainableObject +from apps.user_management.constants import AlertGroupTableColumn from apps.user_management.subscription_strategy import FreePublicBetaSubscriptionStrategy from apps.user_management.utils import default_columns from common.insight_log import ChatOpsEvent, ChatOpsTypePlug, write_chatops_insight_log @@ -286,7 +287,7 @@ def sms_left(self, user): def emails_left(self, user): return self.subscription_strategy.emails_left(user) - def update_alert_group_table_columns(self, columns: list): + def update_alert_group_table_columns(self, columns: typing.List[AlertGroupTableColumn]) -> None: if columns != self.alert_group_table_columns: self.alert_group_table_columns = columns self.save(update_fields=["alert_group_table_columns"]) diff --git a/engine/apps/user_management/models/user.py b/engine/apps/user_management/models/user.py index 418c33f068..9e5b48903b 100644 --- a/engine/apps/user_management/models/user.py +++ b/engine/apps/user_management/models/user.py @@ -21,6 +21,7 @@ user_is_authorized, ) from apps.schedules.tasks import drop_cached_ical_for_custom_events_for_organization +from apps.user_management.constants import AlertGroupTableColumn from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length if typing.TYPE_CHECKING: @@ -451,7 +452,7 @@ def important_notification_policies_defaults(self): ), ) - def update_alert_group_table_columns_settings(self, columns: list): + def update_alert_group_table_columns_settings(self, columns: typing.List[AlertGroupTableColumn]) -> None: if self.alert_groups_table_selected_columns != columns: self.alert_groups_table_selected_columns = columns self.save(update_fields=["alert_groups_table_selected_columns"]) diff --git a/engine/apps/user_management/utils.py b/engine/apps/user_management/utils.py index e98a24c711..8ea909a1bc 100644 --- a/engine/apps/user_management/utils.py +++ b/engine/apps/user_management/utils.py @@ -7,8 +7,11 @@ AlertGroupTableDefaultColumnChoices, ) +if typing.TYPE_CHECKING: + from apps.user_management.models import User -def default_columns(): + +def default_columns() -> typing.List[AlertGroupTableColumn]: columns = [ {"name": column.label, "id": column.value, "type": AlertGroupTableColumnTypeChoices.DEFAULT.value} for column in AlertGroupTableDefaultColumnChoices @@ -16,7 +19,7 @@ def default_columns(): return columns -def alert_group_table_user_settings(user) -> AlertGroupTableColumns: +def alert_group_table_user_settings(user: "User") -> AlertGroupTableColumns: organization_columns = user.organization.alert_group_table_columns visible_columns: typing.List[AlertGroupTableColumn] if user.alert_groups_table_selected_columns: From da31e000af6aa911f1a99f143790fb4821fe5a8a Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Thu, 23 Nov 2023 12:46:59 +0200 Subject: [PATCH 49/67] review --- grafana-plugin/jest.setup.ts | 61 +++++++++---------- grafana-plugin/src/assets/style/utils.css | 4 -- .../WithContextMenu/WithContextMenu.tsx | 9 +-- .../ColumnsSelector/ColumnsSelector.tsx | 21 ++++--- .../ColumnsSelectorWrapper/ColumnsModal.tsx | 10 +-- .../ColumnsSelectorWrapper.styles.ts | 3 + .../ColumnsSelectorWrapper.tsx | 5 +- .../src/models/alertgroup/alertgroup.ts | 2 +- .../src/models/loader/action-keys.ts | 6 +- grafana-plugin/src/models/loader/loader.ts | 3 +- .../escalation-chains/EscalationChains.tsx | 2 +- .../src/pages/incidents/Incidents.module.scss | 4 ++ .../src/pages/incidents/Incidents.tsx | 32 ++++++---- .../src/state/rootBaseStore/index.ts | 2 + grafana-plugin/src/state/useStore.ts | 1 - 15 files changed, 85 insertions(+), 80 deletions(-) diff --git a/grafana-plugin/jest.setup.ts b/grafana-plugin/jest.setup.ts index 1e0ab110e7..4ae785dadc 100644 --- a/grafana-plugin/jest.setup.ts +++ b/grafana-plugin/jest.setup.ts @@ -20,34 +20,33 @@ Object.defineProperty(window, 'matchMedia', { })), }); -const global = window as any; - -global.ResizeObserver = class ResizeObserver { - //callback: ResizeObserverCallback; - - constructor(callback: ResizeObserverCallback) { - setTimeout(() => { - callback( - [ - { - contentRect: { - x: 1, - y: 2, - width: 500, - height: 500, - top: 100, - bottom: 0, - left: 100, - right: 0, - }, - target: {}, - } as ResizeObserverEntry, - ], - this - ); - }); - } - observe() {} - disconnect() {} - unobserve() {} -}; \ No newline at end of file +Object.defineProperty(window, 'ResizeObserver', { + writable: true, + value: class ResizeObserver { + constructor(callback: ResizeObserverCallback) { + setTimeout(() => { + callback( + [ + { + contentRect: { + x: 1, + y: 2, + width: 500, + height: 500, + top: 100, + bottom: 0, + left: 100, + right: 0, + }, + target: {}, + } as ResizeObserverEntry, + ], + this + ); + }); + } + observe() {} + disconnect() {} + unobserve() {} + }, +}); diff --git a/grafana-plugin/src/assets/style/utils.css b/grafana-plugin/src/assets/style/utils.css index 09407b3ceb..7b02d032fd 100644 --- a/grafana-plugin/src/assets/style/utils.css +++ b/grafana-plugin/src/assets/style/utils.css @@ -117,10 +117,6 @@ * Icons */ -.loader { - margin-bottom: 0 !important; -} - .icon-exclamation { color: var(--error-text-color); } diff --git a/grafana-plugin/src/components/WithContextMenu/WithContextMenu.tsx b/grafana-plugin/src/components/WithContextMenu/WithContextMenu.tsx index 1c54785fb2..738af92ef3 100644 --- a/grafana-plugin/src/components/WithContextMenu/WithContextMenu.tsx +++ b/grafana-plugin/src/components/WithContextMenu/WithContextMenu.tsx @@ -51,10 +51,9 @@ export const WithContextMenu: React.FC = ({ }, })} - {isContextMenuOpen() && ( + {isMenuOpen && ( !forceIsOpen && setIsMenuOpen(false)} + onClose={() => setIsMenuOpen(false)} x={menuPosition.x} y={menuPosition.y} renderMenuItems={() => renderMenuItems({ closeMenu: () => setIsMenuOpen(false) })} @@ -63,8 +62,4 @@ export const WithContextMenu: React.FC = ({ )}
    ); - - function isContextMenuOpen() { - return isOpen === undefined ? isMenuOpen : isOpen; - } }; diff --git a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx index 6dd6d1fd41..056f14b74a 100644 --- a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx +++ b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx @@ -22,11 +22,11 @@ import { isEqual } from 'lodash-es'; import { observer } from 'mobx-react'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; +import RenderConditionally from 'components/RenderConditionally/RenderConditionally'; import Text from 'components/Text/Text'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; import { AGColumn, AGColumnType } from 'models/alertgroup/alertgroup.types'; import { ActionKey } from 'models/loader/action-keys'; -import { LoaderStore } from 'models/loader/loader'; import { useStore } from 'state/useStore'; import { openErrorNotification } from 'utils'; import { UserActions } from 'utils/authorization'; @@ -66,7 +66,7 @@ const ColumnRow: React.FC = ({ column, onItemChange, onColumnRem )} - {column.isVisible ? ( + = ({ column, onItemChange, onColumnRem {...attributes} {...listeners} /> - ) : column.type === AGColumnType.LABEL ? ( + + + = ({ column, onItemChange, onColumnRem onClick={() => onColumnRemoval(column)} /> - ) : undefined} +
    = observer( ({ onColumnAddModalOpen, onConfirmRemovalModalOpen }) => { - const { alertGroupStore } = useStore(); + const { alertGroupStore, loaderStore } = useStore(); const styles = useStyles2(getColumnsSelectorStyles); @@ -124,7 +126,7 @@ export const ColumnsSelector: React.FC = observer( }) ); - const isResetLoading = LoaderStore.isLoading(ActionKey.IS_RESETING_COLUMNS_FROM_ALERT_GROUP); + const isResetLoading = loaderStore.isLoading(ActionKey.RESET_COLUMNS_FROM_ALERT_GROUP); return (
    @@ -194,7 +196,7 @@ export const ColumnsSelector: React.FC = observer( tooltipPlacement="top" tooltip={'Reset table to default columns'} disabled={!canResetData || isResetLoading} - onClick={WrapAutoLoadingState(onReset, ActionKey.IS_RESETING_COLUMNS_FROM_ALERT_GROUP)} + onClick={WrapAutoLoadingState(onReset, ActionKey.RESET_COLUMNS_FROM_ALERT_GROUP)} > {isResetLoading ? : 'Reset'} @@ -210,9 +212,8 @@ export const ColumnsSelector: React.FC = observer( async function onReset() { const columnsDefaultValues = getDefaultData(); - return alertGroupStore - .updateTableSettings(columnsDefaultValues, true) - .then(() => alertGroupStore.fetchTableSettings()); + await alertGroupStore.updateTableSettings(columnsDefaultValues, true); + await alertGroupStore.fetchTableSettings(); } function getDefaultData() { diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx index 04caf5d453..e755c7139d 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx @@ -20,7 +20,6 @@ import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/W import { AGColumn, AGColumnType } from 'models/alertgroup/alertgroup.types'; import { Label, LabelValue } from 'models/label/label.types'; import { ActionKey } from 'models/loader/action-keys'; -import { LoaderStore } from 'models/loader/loader'; import { useStore } from 'state/useStore'; import { openErrorNotification, pluralize } from 'utils'; import { UserActions } from 'utils/authorization'; @@ -51,7 +50,7 @@ export const ColumnsModal: React.FC = observer( const [searchResults, setSearchResults] = useState([]); const debouncedOnInputChange = useDebouncedCallback(onInputChange, DEBOUNCE_MS); - const isLoading = LoaderStore.isLoading(ActionKey.IS_ADDING_NEW_COLUMN_TO_ALERT_GROUP); + const isLoading = store.loaderStore.isLoading(ActionKey.ADD_NEW_COLUMN_TO_ALERT_GROUP); const availableKeysForSearching = useMemo(() => { const currentAGColumns = store.alertGroupStore.columns.map((col) => col.name); @@ -91,6 +90,7 @@ export const ColumnsModal: React.FC = observer( { setSearchResults((items) => { @@ -183,9 +183,9 @@ export const ColumnsModal: React.FC = observer( ...searchResults .filter((item) => item.isChecked) .map( - (it): AGColumn => ({ - id: it.id, - name: it.name, + (item): AGColumn => ({ + id: item.id, + name: item.name, isVisible: false, type: AGColumnType.LABEL, }) diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.styles.ts b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.styles.ts index 5c2e978d4c..23a55dcd90 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.styles.ts +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.styles.ts @@ -42,5 +42,8 @@ export const getColumnsSelectorWrapperStyles = (theme: GrafanaTheme2) => { floatingContentVisible: css` display: block; `, + checkboxAddOption: css` + top: 3px; + `, }; }; diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx index c16948f400..b8c06dacdf 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx @@ -10,7 +10,6 @@ import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/W import { AGColumn } from 'models/alertgroup/alertgroup.types'; import { Label } from 'models/label/label.types'; import { ActionKey } from 'models/loader/action-keys'; -import { LoaderStore } from 'models/loader/loader'; import { useStore } from 'state/useStore'; import { UserActions } from 'utils/authorization'; import { WrapAutoLoadingState } from 'utils/decorators'; @@ -50,7 +49,7 @@ const ColumnsSelectorWrapper: React.FC = observer(( }; }, []); - const isRemoveLoading = LoaderStore.isLoading(ActionKey.IS_REMOVING_COLUMN_FROM_ALERT_GROUP); + const isRemoveLoading = store.loaderStore.isLoading(ActionKey.REMOVE_COLUMN_FROM_ALERT_GROUP); return ( <> @@ -79,7 +78,7 @@ const ColumnsSelectorWrapper: React.FC = observer(( diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.ts b/grafana-plugin/src/models/alertgroup/alertgroup.ts index a847108edd..473193739e 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.ts @@ -454,7 +454,7 @@ export class AlertGroupStore extends BaseStore { } @action - @AutoLoadingState(ActionKey.IS_ADDING_NEW_COLUMN_TO_ALERT_GROUP) + @AutoLoadingState(ActionKey.ADD_NEW_COLUMN_TO_ALERT_GROUP) public async updateTableSettings( columns: { visible: AGColumn[]; hidden: AGColumn[] }, isUserUpdate: boolean diff --git a/grafana-plugin/src/models/loader/action-keys.ts b/grafana-plugin/src/models/loader/action-keys.ts index a03903c788..3d5898d8df 100644 --- a/grafana-plugin/src/models/loader/action-keys.ts +++ b/grafana-plugin/src/models/loader/action-keys.ts @@ -1,5 +1,5 @@ export enum ActionKey { - IS_ADDING_NEW_COLUMN_TO_ALERT_GROUP = 'IS_ADDING_NEW_COLUMN_TO_ALERT_GROUP', - IS_REMOVING_COLUMN_FROM_ALERT_GROUP = 'IS_REMOVING_COLUMN_FROM_ALERT_GROUP', - IS_RESETING_COLUMNS_FROM_ALERT_GROUP = 'IS_RESETING_COLUMNS_FROM_ALERT_GROUP', + ADD_NEW_COLUMN_TO_ALERT_GROUP = 'ADD_NEW_COLUMN_TO_ALERT_GROUP', + REMOVE_COLUMN_FROM_ALERT_GROUP = 'REMOVE_COLUMN_FROM_ALERT_GROUP', + RESET_COLUMNS_FROM_ALERT_GROUP = 'RESET_COLUMNS_FROM_ALERT_GROUP', } diff --git a/grafana-plugin/src/models/loader/loader.ts b/grafana-plugin/src/models/loader/loader.ts index 1edcfbdb1b..4606bb454d 100644 --- a/grafana-plugin/src/models/loader/loader.ts +++ b/grafana-plugin/src/models/loader/loader.ts @@ -1,4 +1,4 @@ -import { observable } from 'mobx'; +import { action, observable } from 'mobx'; interface LoadingResult { [key: string]: boolean; @@ -8,6 +8,7 @@ class LoaderStoreClass { @observable items: LoadingResult = {}; + @action setLoadingAction(actionKey: string, isLoading: boolean) { this.items[actionKey] = isLoading; } diff --git a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx index 872e4c16cb..6ddca9aeb3 100644 --- a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx +++ b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx @@ -177,7 +177,7 @@ class EscalationChainsPage extends React.Component ) : ( - + Loading... diff --git a/grafana-plugin/src/pages/incidents/Incidents.module.scss b/grafana-plugin/src/pages/incidents/Incidents.module.scss index 5b1062ff72..3281f8ff9c 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.module.scss +++ b/grafana-plugin/src/pages/incidents/Incidents.module.scss @@ -56,6 +56,10 @@ right: 0; } +.btn-results { + margin-left: 8px; +} + /* filter cards */ .cards { diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index 2768720e34..4d65d995b8 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -15,6 +15,7 @@ import GTable from 'components/GTable/GTable'; import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo'; import ManualAlertGroup from 'components/ManualAlertGroup/ManualAlertGroup'; import PluginLink from 'components/PluginLink/PluginLink'; +import RenderConditionally from 'components/RenderConditionally/RenderConditionally'; import Text from 'components/Text/Text'; import TextEllipsisTooltip from 'components/TextEllipsisTooltip/TextEllipsisTooltip'; import TooltipBadge from 'components/TooltipBadge/TooltipBadge'; @@ -74,6 +75,14 @@ const PAGINATION_OPTIONS = [ { label: '100', value: 100 }, ]; +const TABLE_SCROLL_OPTIONS: Array<{ value: boolean; icon: string }> = [ + { value: false, icon: 'wrap-text' }, + { + value: true, + icon: 'arrow-from-right', + }, +]; + @observer class Incidents extends React.Component { constructor(props: IncidentsPageProps) { @@ -451,11 +460,11 @@ class Incidents extends React.Component
    - {hasInvalidatedAlert && ( + Results out of date - )} + - {store.hasFeature(AppFeature.Labels) && ( + - )} + - {store.hasFeature(AppFeature.Labels) && } + + +
    @@ -801,6 +806,7 @@ class Incidents extends React.Component } return { + width: isHorizontalScrolling ? '200px' : '10%', title: capitalize(column.name), key: column.id.toString(), render: (item: AlertType) => this.renderCustomColumn(column, item), diff --git a/grafana-plugin/src/state/rootBaseStore/index.ts b/grafana-plugin/src/state/rootBaseStore/index.ts index 6a771e2561..9f3d73161b 100644 --- a/grafana-plugin/src/state/rootBaseStore/index.ts +++ b/grafana-plugin/src/state/rootBaseStore/index.ts @@ -19,6 +19,7 @@ import { GlobalSettingStore } from 'models/global_setting/global_setting'; import { GrafanaTeamStore } from 'models/grafana_team/grafana_team'; import { HeartbeatStore } from 'models/heartbeat/heartbeat'; import { LabelStore } from 'models/label/label'; +import { LoaderStore } from 'models/loader/loader'; import { OrganizationStore } from 'models/organization/organization'; import { OutgoingWebhookStore } from 'models/outgoing_webhook/outgoing_webhook'; import { ResolutionNotesStore } from 'models/resolution_note/resolution_note'; @@ -105,6 +106,7 @@ export class RootBaseStore { globalSettingStore = new GlobalSettingStore(this); filtersStore = new FiltersStore(this); labelsStore = new LabelStore(this); + loaderStore = LoaderStore; // stores diff --git a/grafana-plugin/src/state/useStore.ts b/grafana-plugin/src/state/useStore.ts index 9440c5b150..ebcda3baf4 100644 --- a/grafana-plugin/src/state/useStore.ts +++ b/grafana-plugin/src/state/useStore.ts @@ -6,6 +6,5 @@ import { RootStore } from './index'; export function useStore(): RootStore { const { store } = React.useContext(MobXProviderContext); - return store; } From f8bb5aa0b0edacc40f98709e3c98251dafe71582 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Thu, 23 Nov 2023 12:53:16 +0200 Subject: [PATCH 50/67] build fix --- grafana-plugin/src/containers/Labels/Labels.tsx | 4 ---- grafana-plugin/src/pages/incidents/Incidents.tsx | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/grafana-plugin/src/containers/Labels/Labels.tsx b/grafana-plugin/src/containers/Labels/Labels.tsx index 23f2d44a50..faa3e5d935 100644 --- a/grafana-plugin/src/containers/Labels/Labels.tsx +++ b/grafana-plugin/src/containers/Labels/Labels.tsx @@ -9,10 +9,6 @@ import { LabelKeyValue } from 'models/label/label.types'; import { useStore } from 'state/useStore'; import { openErrorNotification } from 'utils'; -import styles from './Labels.module.css'; - -const cx = cn.bind(styles); - export interface LabelsProps { value: LabelKeyValue[]; errors: any; diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index 79cfb77f05..4d65d995b8 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -13,12 +13,12 @@ import CardButton from 'components/CardButton/CardButton'; import CursorPagination from 'components/CursorPagination/CursorPagination'; import GTable from 'components/GTable/GTable'; import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo'; -import LabelsTooltipBadge from 'components/LabelsTooltipBadge/LabelsTooltipBadge'; import ManualAlertGroup from 'components/ManualAlertGroup/ManualAlertGroup'; import PluginLink from 'components/PluginLink/PluginLink'; import RenderConditionally from 'components/RenderConditionally/RenderConditionally'; import Text from 'components/Text/Text'; import TextEllipsisTooltip from 'components/TextEllipsisTooltip/TextEllipsisTooltip'; +import TooltipBadge from 'components/TooltipBadge/TooltipBadge'; import Tutorial from 'components/Tutorial/Tutorial'; import { TutorialStep } from 'components/Tutorial/Tutorial.types'; import ColumnsSelectorWrapper from 'containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper'; From 032fed691b40fca40270979ca1e7dcc34319bec6 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Thu, 23 Nov 2023 12:59:50 +0200 Subject: [PATCH 51/67] increase table horizontal size --- grafana-plugin/src/pages/incidents/Incidents.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index 4d65d995b8..9880448701 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -536,7 +536,7 @@ class Incidents extends React.Component data={results} columns={tableColumns} tableLayout="auto" - scroll={{ x: isHorizontalScrolling ? `${Math.max(2000, tableColumns.length * 200)}px` : true }} + scroll={{ x: isHorizontalScrolling ? `${Math.max(2000, tableColumns.length * 250)}px` : true }} /> {this.shouldShowPagination() && (
    From 6100116a892fa5cf34198137a309f1a29aed04c3 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Thu, 23 Nov 2023 14:25:57 +0200 Subject: [PATCH 52/67] e2e fix --- grafana-plugin/e2e-tests/utils/alertGroup.ts | 2 +- grafana-plugin/src/pages/incidents/Incidents.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/grafana-plugin/e2e-tests/utils/alertGroup.ts b/grafana-plugin/e2e-tests/utils/alertGroup.ts index 1a7d854d83..c56723d37e 100644 --- a/grafana-plugin/e2e-tests/utils/alertGroup.ts +++ b/grafana-plugin/e2e-tests/utils/alertGroup.ts @@ -68,7 +68,7 @@ export const filterAlertGroupsTableByIntegrationAndGoToDetailPage = async ( * recursively retry this function */ await firstTableRow.getByText(integrationName).waitFor({ state: 'visible', timeout: 5000 }); - await firstTableRow.locator('td:nth-child(4) a').click(); + await firstTableRow.getByTestId('integration-name').click(); } catch (err) { return filterAlertGroupsTableByIntegrationAndGoToDetailPage(page, integrationName, (retryNum += 1)); } diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index 9880448701..858e3ab1f1 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -610,7 +610,7 @@ class Incidents extends React.Component content={record?.alert_receive_channel?.verbal_name || ''} > - + ); }; From f9ab3cd73994010f91276a175a82d4fb1ea97db6 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Thu, 23 Nov 2023 16:31:11 +0200 Subject: [PATCH 53/67] attempt to fix e2e failing case --- CHANGELOG.md | 4 ++++ grafana-plugin/e2e-tests/utils/alertGroup.ts | 15 ++++++--------- .../ColumnsSelector/ColumnsSelector.styles.ts | 4 ++-- grafana-plugin/src/pages/incidents/Incidents.tsx | 8 ++++++-- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d201ce398..2f1cfaf0c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Add options to customize table columns in AlertGroup page ([3281](https://github.com/grafana/oncall/pull/3281)) + ### Fixed - User filter doesn't display current value on Alert Groups page ([1714](https://github.com/grafana/oncall/issues/1714)) diff --git a/grafana-plugin/e2e-tests/utils/alertGroup.ts b/grafana-plugin/e2e-tests/utils/alertGroup.ts index c56723d37e..33ef246282 100644 --- a/grafana-plugin/e2e-tests/utils/alertGroup.ts +++ b/grafana-plugin/e2e-tests/utils/alertGroup.ts @@ -55,20 +55,17 @@ export const filterAlertGroupsTableByIntegrationAndGoToDetailPage = async ( await selectElement.type(integrationName); await selectValuePickerValue(page, integrationName, false); - /** - * wait for the alert groups to be filtered then by this particular integration (toBeVisible assertion), - * then click on the alert group and go to the individual alert group page - */ - const firstTableRow = page.locator('table > tbody > tr:first-child'); - try { /** - * wait for up to 5 seconds for the alert groups to be filtered, if the first row does not correspond + * wait for up to 2 seconds for the alert groups to be filtered, if the first row does not correspond * to `integrationName` assume that the background workers have not created it yet and lets * recursively retry this function */ - await firstTableRow.getByText(integrationName).waitFor({ state: 'visible', timeout: 5000 }); - await firstTableRow.getByTestId('integration-name').click(); + + await page.waitForTimeout(2000); + + expect(await page.locator('table > tbody > tr [data-testid=integration-name]').textContent()).toBe(integrationName); + await page.locator('table > tbody > tr [data-testid=integration-url]').click(); } catch (err) { return filterAlertGroupsTableByIntegrationAndGoToDetailPage(page, integrationName, (retryNum += 1)); } diff --git a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.styles.ts b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.styles.ts index 50593f1a5a..5d78b09a99 100644 --- a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.styles.ts +++ b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.styles.ts @@ -86,8 +86,8 @@ export const getColumnsSelectorStyles = (_theme: GrafanaTheme2) => { `, labelIcon: css` - margin: 0 !important; - padding: 0 !important; + margin: 0; + padding: 0; `, }; }; diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index 858e3ab1f1..54efde5c9a 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -573,7 +573,7 @@ class Incidents extends React.Component return (
    - + content={record?.alert_receive_channel?.verbal_name || ''} > - + ); }; From 463c43b462478b3db17a75758d80b224b503046b Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Thu, 23 Nov 2023 16:31:20 +0200 Subject: [PATCH 54/67] review --- .../src/components/WithContextMenu/WithContextMenu.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/grafana-plugin/src/components/WithContextMenu/WithContextMenu.tsx b/grafana-plugin/src/components/WithContextMenu/WithContextMenu.tsx index 738af92ef3..c593f14f6e 100644 --- a/grafana-plugin/src/components/WithContextMenu/WithContextMenu.tsx +++ b/grafana-plugin/src/components/WithContextMenu/WithContextMenu.tsx @@ -16,7 +16,6 @@ const query = '[class$="-page-container"] .scrollbar-view'; export const WithContextMenu: React.FC = ({ children, renderMenuItems, - isOpen, forceIsOpen = false, focusOnOpen = true, ...rest From 7948371931ef65aa74aead7727bf598669a0305d Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 27 Nov 2023 12:55:01 +0100 Subject: [PATCH 55/67] Add endpoint to reset columns settings, after review fixes --- .../serializers/alert_group_table_settings.py | 56 ++++++++----------- engine/apps/api/urls.py | 9 ++- .../api/views/alert_group_table_settings.py | 39 +++++++------ engine/apps/user_management/constants.py | 24 ++++---- engine/apps/user_management/utils.py | 3 +- 5 files changed, 64 insertions(+), 67 deletions(-) diff --git a/engine/apps/api/serializers/alert_group_table_settings.py b/engine/apps/api/serializers/alert_group_table_settings.py index 6c3ce04aa7..ab06997675 100644 --- a/engine/apps/api/serializers/alert_group_table_settings.py +++ b/engine/apps/api/serializers/alert_group_table_settings.py @@ -4,20 +4,9 @@ from apps.user_management.constants import AlertGroupTableColumnTypeChoices, AlertGroupTableDefaultColumnChoices -class ColumnIdField(serializers.Field): - def to_representation(self, value): - return value - - def to_internal_value(self, data): - if isinstance(data, int) or isinstance(data, str): - return data - else: - raise ValidationError("Invalid column id format") - - class AlertGroupTableColumnSerializer(serializers.Serializer): name = serializers.CharField(max_length=200) - id = ColumnIdField() + id = serializers.CharField(max_length=200) type = serializers.ChoiceField(choices=AlertGroupTableColumnTypeChoices.choices) def validate(self, data): @@ -25,6 +14,7 @@ def validate(self, data): return data def _validate_id(self, data): + """Validate if `id` of column with `default` type is in the list of available default columns""" if ( data["type"] == AlertGroupTableColumnTypeChoices.DEFAULT.value and data["id"] not in AlertGroupTableDefaultColumnChoices.values @@ -32,34 +22,34 @@ def _validate_id(self, data): raise ValidationError("Invalid column id format") -class AlertGroupTableColumnsListSerializer(serializers.Serializer): +class AlertGroupTableColumnsOrganizationSerializer(serializers.Serializer): visible = AlertGroupTableColumnSerializer(many=True) hidden = AlertGroupTableColumnSerializer(many=True) def validate(self, data): """ - Validate data regarding if it updates alert group table columns settings for organization or for user - and validate that at least one column is selected as visible. + Validate that at least one column is selected as visible and that all default columns are in the list. + """ + columns = data["visible"] + data["hidden"] + request_columns_ids = [column["id"] for column in columns] + if not set(request_columns_ids) >= set(AlertGroupTableDefaultColumnChoices.values): + raise ValidationError("Default column cannot be removed") + elif len(request_columns_ids) > len(set(request_columns_ids)): + raise ValidationError("Duplicate column") + return data - `is_org_settings=True` means that organization alert group table columns list should be updated. - Validate that all default columns are in the list. - `is_org_settings=False` means that list of visible columns for user should be updated. +class AlertGroupTableColumnsUserSerializer(AlertGroupTableColumnsOrganizationSerializer): + def validate(self, data): + """ Validate that all columns exist in organization alert group table columns list. """ - is_org_settings = self.context.get("is_org_settings") is True - organization = self.context["request"].auth.organization - columns_list = data["visible"] + data["hidden"] - request_columns_ids = [column["id"] for column in columns_list] - if len(data["visible"]) == 0: - raise ValidationError("At least one column should be selected as visible") - if is_org_settings: - if not set(request_columns_ids) >= set(AlertGroupTableDefaultColumnChoices.values): - raise ValidationError("Default column cannot be removed") - elif len(request_columns_ids) > len(set(request_columns_ids)): - raise ValidationError("Duplicate column") - else: - organization_columns_ids = [column["id"] for column in organization.alert_group_table_columns] - if set(organization_columns_ids) != set(request_columns_ids): - raise ValidationError("Invalid settings") + data = super().validate(data) + columns = data["visible"] + data["hidden"] + request_columns_ids = [column["id"] for column in columns] + organization_columns_ids = [ + column["id"] for column in self.context["request"].auth.organization.alert_group_table_columns + ] + if set(organization_columns_ids) != set(request_columns_ids): + raise ValidationError("Invalid settings") return data diff --git a/engine/apps/api/urls.py b/engine/apps/api/urls.py index 0091d987ba..80137dc074 100644 --- a/engine/apps/api/urls.py +++ b/engine/apps/api/urls.py @@ -150,8 +150,13 @@ re_path( r"^alertgroup_table_settings/?$", AlertGroupTableColumnsViewSet.as_view( - {"get": "get_columns", "put": "update_columns_settings", "post": "update_columns_list"} + {"get": "get_columns", "put": "update_user_columns", "post": "update_organization_columns"} ), name="alert_group_table-columns_settings", - ) + ), + re_path( + r"^alertgroup_table_settings/reset?$", + AlertGroupTableColumnsViewSet.as_view({"post": "reset_user_columns"}), + name="alert_group_table-reset_columns_settings", + ), ] diff --git a/engine/apps/api/views/alert_group_table_settings.py b/engine/apps/api/views/alert_group_table_settings.py index 48ecc0d933..3b722981d3 100644 --- a/engine/apps/api/views/alert_group_table_settings.py +++ b/engine/apps/api/views/alert_group_table_settings.py @@ -5,11 +5,14 @@ from rest_framework.response import Response from apps.api.permissions import RBACPermission -from apps.api.serializers.alert_group_table_settings import AlertGroupTableColumnsListSerializer +from apps.api.serializers.alert_group_table_settings import ( + AlertGroupTableColumnsOrganizationSerializer, + AlertGroupTableColumnsUserSerializer, +) from apps.api.views.labels import LabelsFeatureFlagViewSet from apps.auth_token.auth import PluginAuthentication from apps.user_management.constants import AlertGroupTableColumn -from apps.user_management.utils import alert_group_table_user_settings +from apps.user_management.utils import alert_group_table_user_settings, default_columns class AlertGroupTableColumnsViewSet(LabelsFeatureFlagViewSet): @@ -18,35 +21,35 @@ class AlertGroupTableColumnsViewSet(LabelsFeatureFlagViewSet): rbac_permissions = { "get_columns": [RBACPermission.Permissions.ALERT_GROUPS_READ], - "update_columns_settings": [RBACPermission.Permissions.ALERT_GROUPS_READ], - "update_columns_list": [RBACPermission.Permissions.OTHER_SETTINGS_WRITE], + "update_user_columns": [RBACPermission.Permissions.ALERT_GROUPS_READ], + "reset_user_columns": [RBACPermission.Permissions.ALERT_GROUPS_READ], + "update_organization_columns": [RBACPermission.Permissions.OTHER_SETTINGS_WRITE], } def get_columns(self, request: Request) -> Response: - user = request.user - return Response(alert_group_table_user_settings(user)) + return Response(alert_group_table_user_settings(request.user)) - def update_columns_list(self, request: Request) -> Response: + def update_organization_columns(self, request: Request) -> Response: """add/remove columns for organization""" - user = request.user - organization = request.auth.organization - serializer = AlertGroupTableColumnsListSerializer( - data=request.data, context={"request": request, "is_org_settings": True} - ) + serializer = AlertGroupTableColumnsOrganizationSerializer(data=request.data, context={"request": request}) serializer.is_valid(raise_exception=True) columns: typing.List[AlertGroupTableColumn] = serializer.validated_data.get( "visible", [] ) + serializer.validated_data.get("hidden", []) - organization.update_alert_group_table_columns(columns) - return Response(alert_group_table_user_settings(user)) + request.auth.organization.update_alert_group_table_columns(columns) + return Response(alert_group_table_user_settings(request.user)) - def update_columns_settings(self, request: Request) -> Response: + def update_user_columns(self, request: Request) -> Response: """select/hide/change order for user""" user = request.user - serializer = AlertGroupTableColumnsListSerializer( - data=request.data, context={"request": request, "is_org_settings": False} - ) + serializer = AlertGroupTableColumnsUserSerializer(data=request.data, context={"request": request}) serializer.is_valid(raise_exception=True) columns: typing.List[AlertGroupTableColumn] = serializer.validated_data.get("visible", []) user.update_alert_group_table_columns_settings(columns) return Response(alert_group_table_user_settings(user)) + + def reset_user_columns(self, request: Request) -> Response: + """set default alert group table settings for user""" + user = request.user + user.update_alert_group_table_columns_settings(default_columns()) + return Response(alert_group_table_user_settings(user)) diff --git a/engine/apps/user_management/constants.py b/engine/apps/user_management/constants.py index 9b09356c61..1de4f86c76 100644 --- a/engine/apps/user_management/constants.py +++ b/engine/apps/user_management/constants.py @@ -1,18 +1,18 @@ import typing -from django.db.models import IntegerChoices, TextChoices +from django.db.models import TextChoices -class AlertGroupTableDefaultColumnChoices(IntegerChoices): - STATUS = 1, "Status" - ID = 2, "ID" - TITLE = 3, "Title" - ALERTS = 4, "Alerts" - INTEGRATION = 5, "Integration" - CREATED = 6, "Created" - LABELS = 7, "Labels" - TEAM = 8, "Team" - USERS = 9, "Users" +class AlertGroupTableDefaultColumnChoices(TextChoices): + STATUS = "status", "Status" + ID = "id", "ID" + TITLE = "title", "Title" + ALERTS = "alerts", "Alerts" + INTEGRATION = "integration", "Integration" + CREATED = "created", "Created" + LABELS = "labels", "Labels" + TEAM = "team", "Team" + USERS = "users", "Users" class AlertGroupTableColumnTypeChoices(TextChoices): @@ -21,7 +21,7 @@ class AlertGroupTableColumnTypeChoices(TextChoices): class AlertGroupTableColumn(typing.TypedDict): - id: str | int + id: str name: str type: str diff --git a/engine/apps/user_management/utils.py b/engine/apps/user_management/utils.py index 8ea909a1bc..c0fd86b6c8 100644 --- a/engine/apps/user_management/utils.py +++ b/engine/apps/user_management/utils.py @@ -12,11 +12,10 @@ def default_columns() -> typing.List[AlertGroupTableColumn]: - columns = [ + return [ {"name": column.label, "id": column.value, "type": AlertGroupTableColumnTypeChoices.DEFAULT.value} for column in AlertGroupTableDefaultColumnChoices ] - return columns def alert_group_table_user_settings(user: "User") -> AlertGroupTableColumns: From 8d9ae1a09165b728be7326eb40c952f6895ab454 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 27 Nov 2023 16:51:51 +0100 Subject: [PATCH 56/67] Fix columns settings validation, add tests --- .../serializers/alert_group_table_settings.py | 4 +- .../tests/test_alert_group_table_settings.py | 44 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/engine/apps/api/serializers/alert_group_table_settings.py b/engine/apps/api/serializers/alert_group_table_settings.py index ab06997675..160ff8e337 100644 --- a/engine/apps/api/serializers/alert_group_table_settings.py +++ b/engine/apps/api/serializers/alert_group_table_settings.py @@ -32,7 +32,9 @@ def validate(self, data): """ columns = data["visible"] + data["hidden"] request_columns_ids = [column["id"] for column in columns] - if not set(request_columns_ids) >= set(AlertGroupTableDefaultColumnChoices.values): + if len(data["visible"]) == 0: + raise ValidationError("At least one column should be selected as visible") + elif not set(request_columns_ids) >= set(AlertGroupTableDefaultColumnChoices.values): raise ValidationError("Default column cannot be removed") elif len(request_columns_ids) > len(set(request_columns_ids)): raise ValidationError("Duplicate column") diff --git a/engine/apps/api/tests/test_alert_group_table_settings.py b/engine/apps/api/tests/test_alert_group_table_settings.py index 4ae106484d..bd1632c32b 100644 --- a/engine/apps/api/tests/test_alert_group_table_settings.py +++ b/engine/apps/api/tests/test_alert_group_table_settings.py @@ -136,6 +136,26 @@ def test_update_columns_settings( assert response.json() == updated_columns_settings +@pytest.mark.django_db +def test_reset_user_columns( + make_organization_and_user_with_plugin_token, + make_user_auth_headers, +): + """Test reset alert group table settings for user""" + organization, user, token = make_organization_and_user_with_plugin_token() + client = APIClient() + url = reverse("api-internal:alert_group_table-reset_columns_settings") + new_column = {"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value} + organization.alert_group_table_columns += [new_column] + organization.save() + user.update_alert_group_table_columns_settings(organization.alert_group_table_columns[1::-1]) + default_settings = columns_settings(new_column) + assert alert_group_table_user_settings(user) != default_settings + response = client.post(url, format="json", **make_user_auth_headers(user, token)) + assert response.status_code == status.HTTP_200_OK + assert response.json() == default_settings + + @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", @@ -208,3 +228,27 @@ def test_update_columns_settings_permissions( response = client.put(url, data=data, format="json", **make_user_auth_headers(user, token)) assert response.status_code == expected_status + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "role,expected_status", + [ + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN), + ], +) +def test_reset_user_columns_permissions( + role, + expected_status, + make_organization_and_user_with_plugin_token, + make_user_auth_headers, +): + organization, user, token = make_organization_and_user_with_plugin_token(role) + client = APIClient() + url = reverse("api-internal:alert_group_table-reset_columns_settings") + response = client.post(url, format="json", **make_user_auth_headers(user, token)) + + assert response.status_code == expected_status From 07ddacf7ea70752e9a22ad3dc189203fc85ff5f3 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 28 Nov 2023 12:37:01 +0100 Subject: [PATCH 57/67] reorginize utils for alert group columns settings --- .../utils.py => api/alert_group_table_columns.py} | 14 +------------- .../api/tests/test_alert_group_table_settings.py | 4 ++-- .../apps/api/views/alert_group_table_settings.py | 4 ++-- engine/apps/user_management/constants.py | 7 +++++++ .../migrations/0018_auto_20231115_1206.py | 4 ++-- engine/apps/user_management/models/organization.py | 3 +-- .../tests/test_alert_group_table_settings.py | 4 ++-- 7 files changed, 17 insertions(+), 23 deletions(-) rename engine/apps/{user_management/utils.py => api/alert_group_table_columns.py} (65%) diff --git a/engine/apps/user_management/utils.py b/engine/apps/api/alert_group_table_columns.py similarity index 65% rename from engine/apps/user_management/utils.py rename to engine/apps/api/alert_group_table_columns.py index c0fd86b6c8..d4c0e765f6 100644 --- a/engine/apps/user_management/utils.py +++ b/engine/apps/api/alert_group_table_columns.py @@ -1,23 +1,11 @@ import typing -from apps.user_management.constants import ( - AlertGroupTableColumn, - AlertGroupTableColumns, - AlertGroupTableColumnTypeChoices, - AlertGroupTableDefaultColumnChoices, -) +from apps.user_management.constants import AlertGroupTableColumn, AlertGroupTableColumns, default_columns if typing.TYPE_CHECKING: from apps.user_management.models import User -def default_columns() -> typing.List[AlertGroupTableColumn]: - return [ - {"name": column.label, "id": column.value, "type": AlertGroupTableColumnTypeChoices.DEFAULT.value} - for column in AlertGroupTableDefaultColumnChoices - ] - - def alert_group_table_user_settings(user: "User") -> AlertGroupTableColumns: organization_columns = user.organization.alert_group_table_columns visible_columns: typing.List[AlertGroupTableColumn] diff --git a/engine/apps/api/tests/test_alert_group_table_settings.py b/engine/apps/api/tests/test_alert_group_table_settings.py index bd1632c32b..d6835f1ecd 100644 --- a/engine/apps/api/tests/test_alert_group_table_settings.py +++ b/engine/apps/api/tests/test_alert_group_table_settings.py @@ -3,9 +3,9 @@ from rest_framework import status from rest_framework.test import APIClient +from apps.api.alert_group_table_columns import alert_group_table_user_settings from apps.api.permissions import LegacyAccessControlRole -from apps.user_management.constants import AlertGroupTableColumnTypeChoices -from apps.user_management.utils import alert_group_table_user_settings, default_columns +from apps.user_management.constants import AlertGroupTableColumnTypeChoices, default_columns DEFAULT_COLUMNS = default_columns() diff --git a/engine/apps/api/views/alert_group_table_settings.py b/engine/apps/api/views/alert_group_table_settings.py index 3b722981d3..eeafec29db 100644 --- a/engine/apps/api/views/alert_group_table_settings.py +++ b/engine/apps/api/views/alert_group_table_settings.py @@ -4,6 +4,7 @@ from rest_framework.request import Request from rest_framework.response import Response +from apps.api.alert_group_table_columns import alert_group_table_user_settings from apps.api.permissions import RBACPermission from apps.api.serializers.alert_group_table_settings import ( AlertGroupTableColumnsOrganizationSerializer, @@ -11,8 +12,7 @@ ) from apps.api.views.labels import LabelsFeatureFlagViewSet from apps.auth_token.auth import PluginAuthentication -from apps.user_management.constants import AlertGroupTableColumn -from apps.user_management.utils import alert_group_table_user_settings, default_columns +from apps.user_management.constants import AlertGroupTableColumn, default_columns class AlertGroupTableColumnsViewSet(LabelsFeatureFlagViewSet): diff --git a/engine/apps/user_management/constants.py b/engine/apps/user_management/constants.py index 1de4f86c76..58aeaac846 100644 --- a/engine/apps/user_management/constants.py +++ b/engine/apps/user_management/constants.py @@ -29,3 +29,10 @@ class AlertGroupTableColumn(typing.TypedDict): class AlertGroupTableColumns(typing.TypedDict): visible: typing.List[AlertGroupTableColumn] hidden: typing.List[AlertGroupTableColumn] + + +def default_columns() -> typing.List[AlertGroupTableColumn]: + return [ + {"name": column.label, "id": column.value, "type": AlertGroupTableColumnTypeChoices.DEFAULT.value} + for column in AlertGroupTableDefaultColumnChoices + ] diff --git a/engine/apps/user_management/migrations/0018_auto_20231115_1206.py b/engine/apps/user_management/migrations/0018_auto_20231115_1206.py index a4d95a276c..4376b81b5e 100644 --- a/engine/apps/user_management/migrations/0018_auto_20231115_1206.py +++ b/engine/apps/user_management/migrations/0018_auto_20231115_1206.py @@ -1,6 +1,6 @@ # Generated by Django 3.2.20 on 2023-11-15 12:06 -import apps.user_management.utils +import apps.api.alert_group_table_columns from django.db import migrations, models import django_migration_linter as linter @@ -17,7 +17,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='organization', name='alert_group_table_columns', - field=models.JSONField(default=apps.user_management.utils.default_columns), + field=models.JSONField(default=apps.api.alert_group_table_columns.default_columns), ), migrations.AddField( model_name='user', diff --git a/engine/apps/user_management/models/organization.py b/engine/apps/user_management/models/organization.py index c84ba80b9c..9a6de4a6ad 100644 --- a/engine/apps/user_management/models/organization.py +++ b/engine/apps/user_management/models/organization.py @@ -11,9 +11,8 @@ from mirage import fields as mirage_fields from apps.alerts.models import MaintainableObject -from apps.user_management.constants import AlertGroupTableColumn +from apps.user_management.constants import AlertGroupTableColumn, default_columns from apps.user_management.subscription_strategy import FreePublicBetaSubscriptionStrategy -from apps.user_management.utils import default_columns from common.insight_log import ChatOpsEvent, ChatOpsTypePlug, write_chatops_insight_log from common.oncall_gateway import create_oncall_connector, delete_oncall_connector, delete_slack_connector from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length diff --git a/engine/apps/user_management/tests/test_alert_group_table_settings.py b/engine/apps/user_management/tests/test_alert_group_table_settings.py index c46ff2d79e..a3583014f2 100644 --- a/engine/apps/user_management/tests/test_alert_group_table_settings.py +++ b/engine/apps/user_management/tests/test_alert_group_table_settings.py @@ -1,7 +1,7 @@ import pytest -from apps.user_management.constants import AlertGroupTableColumnTypeChoices -from apps.user_management.utils import alert_group_table_user_settings, default_columns +from apps.api.alert_group_table_columns import alert_group_table_user_settings +from apps.user_management.constants import AlertGroupTableColumnTypeChoices, default_columns DEFAULT_COLUMNS = default_columns() From ca68f0587ca539300882dee355cd03fa38d40e98 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 28 Nov 2023 14:43:12 +0100 Subject: [PATCH 58/67] Update alert group table settings field for organization, add lazy update --- engine/apps/api/alert_group_table_columns.py | 19 ++-- .../serializers/alert_group_table_settings.py | 11 ++- .../tests/test_alert_group_table_settings.py | 87 +++++++++++++++++- .../api/views/alert_group_table_settings.py | 4 +- .../migrations/0018_auto_20231115_1206.py | 8 +- .../user_management/models/organization.py | 4 +- engine/apps/user_management/models/user.py | 10 +-- .../tests/test_alert_group_table_settings.py | 88 ------------------- 8 files changed, 113 insertions(+), 118 deletions(-) delete mode 100644 engine/apps/user_management/tests/test_alert_group_table_settings.py diff --git a/engine/apps/api/alert_group_table_columns.py b/engine/apps/api/alert_group_table_columns.py index d4c0e765f6..056d33b11a 100644 --- a/engine/apps/api/alert_group_table_columns.py +++ b/engine/apps/api/alert_group_table_columns.py @@ -1,22 +1,25 @@ import typing -from apps.user_management.constants import AlertGroupTableColumn, AlertGroupTableColumns, default_columns +from apps.user_management.constants import AlertGroupTableColumns, default_columns if typing.TYPE_CHECKING: from apps.user_management.models import User def alert_group_table_user_settings(user: "User") -> AlertGroupTableColumns: + """ + Returns user settings for alert group table columns. + This function uses lazy update to update columns settings for organization and for user. + """ + if not user.organization.alert_group_table_columns: + user.organization.update_alert_group_table_columns(default_columns()) organization_columns = user.organization.alert_group_table_columns - visible_columns: typing.List[AlertGroupTableColumn] - if user.alert_groups_table_selected_columns: + if user.alert_group_table_selected_columns: visible_columns = [ - column for column in user.alert_groups_table_selected_columns if column in organization_columns + column for column in user.alert_group_table_selected_columns if column in organization_columns ] else: visible_columns = default_columns() - user.update_alert_group_table_columns_settings(visible_columns) - hidden_columns: typing.List[AlertGroupTableColumn] = [ - column for column in organization_columns if column not in visible_columns - ] + user.update_alert_group_table_selected_columns(visible_columns) + hidden_columns = [column for column in organization_columns if column not in visible_columns] return {"visible": visible_columns, "hidden": hidden_columns} diff --git a/engine/apps/api/serializers/alert_group_table_settings.py b/engine/apps/api/serializers/alert_group_table_settings.py index 160ff8e337..24dccf1af1 100644 --- a/engine/apps/api/serializers/alert_group_table_settings.py +++ b/engine/apps/api/serializers/alert_group_table_settings.py @@ -1,7 +1,11 @@ from rest_framework import serializers from rest_framework.exceptions import ValidationError -from apps.user_management.constants import AlertGroupTableColumnTypeChoices, AlertGroupTableDefaultColumnChoices +from apps.user_management.constants import ( + AlertGroupTableColumnTypeChoices, + AlertGroupTableDefaultColumnChoices, + default_columns, +) class AlertGroupTableColumnSerializer(serializers.Serializer): @@ -49,9 +53,8 @@ def validate(self, data): data = super().validate(data) columns = data["visible"] + data["hidden"] request_columns_ids = [column["id"] for column in columns] - organization_columns_ids = [ - column["id"] for column in self.context["request"].auth.organization.alert_group_table_columns - ] + organization_columns = self.context["request"].auth.organization.alert_group_table_columns or default_columns() + organization_columns_ids = [column["id"] for column in organization_columns] if set(organization_columns_ids) != set(request_columns_ids): raise ValidationError("Invalid settings") return data diff --git a/engine/apps/api/tests/test_alert_group_table_settings.py b/engine/apps/api/tests/test_alert_group_table_settings.py index d6835f1ecd..092c319a56 100644 --- a/engine/apps/api/tests/test_alert_group_table_settings.py +++ b/engine/apps/api/tests/test_alert_group_table_settings.py @@ -146,9 +146,8 @@ def test_reset_user_columns( client = APIClient() url = reverse("api-internal:alert_group_table-reset_columns_settings") new_column = {"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value} - organization.alert_group_table_columns += [new_column] - organization.save() - user.update_alert_group_table_columns_settings(organization.alert_group_table_columns[1::-1]) + organization.update_alert_group_table_columns(default_columns() + [new_column]) + user.update_alert_group_table_selected_columns(organization.alert_group_table_columns[1::-1]) default_settings = columns_settings(new_column) assert alert_group_table_user_settings(user) != default_settings response = client.post(url, format="json", **make_user_auth_headers(user, token)) @@ -252,3 +251,85 @@ def test_reset_user_columns_permissions( response = client.post(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == expected_status + + +@pytest.mark.parametrize( + "user_settings,organization_settings,expected_result", + [ + # user doesn't have settings, organization has default settings - all columns are visible + ( + None, + DEFAULT_COLUMNS, + {"visible": DEFAULT_COLUMNS, "hidden": []}, + ), + # user doesn't have settings, organization has updated settings - only default columns are visible + ( + None, + DEFAULT_COLUMNS + [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}], + { + "visible": DEFAULT_COLUMNS, + "hidden": [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}], + }, + ), + # user has settings, organization has default settings - only selected columns are visible + ( + DEFAULT_COLUMNS[:3], + DEFAULT_COLUMNS, + {"visible": DEFAULT_COLUMNS[:3], "hidden": DEFAULT_COLUMNS[3:]}, + ), + # user has settings, organization has unchanged settings - only selected columns are visible + ( + DEFAULT_COLUMNS[:3] + + [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}], + DEFAULT_COLUMNS + [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}], + { + "visible": ( + DEFAULT_COLUMNS[:3] + + [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}] + ), + "hidden": DEFAULT_COLUMNS[3:], + }, + ), + # user has settings, organization has updated settings - column was removed, remove from settings + ( + DEFAULT_COLUMNS[:3] + + [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}], + DEFAULT_COLUMNS, + {"visible": DEFAULT_COLUMNS[:3], "hidden": DEFAULT_COLUMNS[3:]}, + ), + # user has settings with reordered columns, organization has unchanged settings - selected columns in particular + # order are visible + ( + [ + DEFAULT_COLUMNS[1], + DEFAULT_COLUMNS[3], + {"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}, + DEFAULT_COLUMNS[2], + ], + DEFAULT_COLUMNS + [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}], + { + "visible": [ + DEFAULT_COLUMNS[1], + DEFAULT_COLUMNS[3], + {"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}, + DEFAULT_COLUMNS[2], + ], + "hidden": DEFAULT_COLUMNS[:1] + DEFAULT_COLUMNS[4:], + }, + ), + ], +) +@pytest.mark.django_db +def test_alert_group_table_user_settings( + user_settings, + organization_settings, + expected_result, + make_organization_and_user, +): + organization, user = make_organization_and_user() + organization.update_alert_group_table_columns(organization_settings) + if user_settings: + user.update_alert_group_table_selected_columns(user_settings) + result = alert_group_table_user_settings(user) + assert result == expected_result + assert user.alert_group_table_selected_columns == result["visible"] diff --git a/engine/apps/api/views/alert_group_table_settings.py b/engine/apps/api/views/alert_group_table_settings.py index eeafec29db..3b0a35f429 100644 --- a/engine/apps/api/views/alert_group_table_settings.py +++ b/engine/apps/api/views/alert_group_table_settings.py @@ -45,11 +45,11 @@ def update_user_columns(self, request: Request) -> Response: serializer = AlertGroupTableColumnsUserSerializer(data=request.data, context={"request": request}) serializer.is_valid(raise_exception=True) columns: typing.List[AlertGroupTableColumn] = serializer.validated_data.get("visible", []) - user.update_alert_group_table_columns_settings(columns) + user.update_alert_group_table_selected_columns(columns) return Response(alert_group_table_user_settings(user)) def reset_user_columns(self, request: Request) -> Response: """set default alert group table settings for user""" user = request.user - user.update_alert_group_table_columns_settings(default_columns()) + user.update_alert_group_table_selected_columns(default_columns()) return Response(alert_group_table_user_settings(user)) diff --git a/engine/apps/user_management/migrations/0018_auto_20231115_1206.py b/engine/apps/user_management/migrations/0018_auto_20231115_1206.py index 4376b81b5e..5e92d9ad0e 100644 --- a/engine/apps/user_management/migrations/0018_auto_20231115_1206.py +++ b/engine/apps/user_management/migrations/0018_auto_20231115_1206.py @@ -1,10 +1,7 @@ # Generated by Django 3.2.20 on 2023-11-15 12:06 -import apps.api.alert_group_table_columns from django.db import migrations, models -import django_migration_linter as linter - class Migration(migrations.Migration): @@ -13,15 +10,14 @@ class Migration(migrations.Migration): ] operations = [ - linter.IgnoreMigration(), migrations.AddField( model_name='organization', name='alert_group_table_columns', - field=models.JSONField(default=apps.api.alert_group_table_columns.default_columns), + field=models.JSONField(default=None, null=True), ), migrations.AddField( model_name='user', - name='alert_groups_table_selected_columns', + name='alert_group_table_selected_columns', field=models.JSONField(default=None, null=True), ), ] diff --git a/engine/apps/user_management/models/organization.py b/engine/apps/user_management/models/organization.py index 9a6de4a6ad..d3a16bf252 100644 --- a/engine/apps/user_management/models/organization.py +++ b/engine/apps/user_management/models/organization.py @@ -11,7 +11,7 @@ from mirage import fields as mirage_fields from apps.alerts.models import MaintainableObject -from apps.user_management.constants import AlertGroupTableColumn, default_columns +from apps.user_management.constants import AlertGroupTableColumn from apps.user_management.subscription_strategy import FreePublicBetaSubscriptionStrategy from common.insight_log import ChatOpsEvent, ChatOpsTypePlug, write_chatops_insight_log from common.oncall_gateway import create_oncall_connector, delete_oncall_connector, delete_slack_connector @@ -249,7 +249,7 @@ def _get_subscription_strategy(self): is_rbac_permissions_enabled = models.BooleanField(default=False) is_grafana_incident_enabled = models.BooleanField(default=False) - alert_group_table_columns = JSONField(default=default_columns) + alert_group_table_columns: list[AlertGroupTableColumn] | None = JSONField(default=None, null=True) class Meta: unique_together = ("stack_id", "org_id") diff --git a/engine/apps/user_management/models/user.py b/engine/apps/user_management/models/user.py index 9e5b48903b..42a942b590 100644 --- a/engine/apps/user_management/models/user.py +++ b/engine/apps/user_management/models/user.py @@ -237,7 +237,7 @@ class Meta: is_active = models.BooleanField(null=True, default=True) permissions = models.JSONField(null=False, default=list) - alert_groups_table_selected_columns = models.JSONField(default=None, null=True) + alert_group_table_selected_columns: list[AlertGroupTableColumn] | None = models.JSONField(default=None, null=True) def __str__(self): return f"{self.pk}: {self.username}" @@ -452,10 +452,10 @@ def important_notification_policies_defaults(self): ), ) - def update_alert_group_table_columns_settings(self, columns: typing.List[AlertGroupTableColumn]) -> None: - if self.alert_groups_table_selected_columns != columns: - self.alert_groups_table_selected_columns = columns - self.save(update_fields=["alert_groups_table_selected_columns"]) + def update_alert_group_table_selected_columns(self, columns: typing.List[AlertGroupTableColumn]) -> None: + if self.alert_group_table_selected_columns != columns: + self.alert_group_table_selected_columns = columns + self.save(update_fields=["alert_group_table_selected_columns"]) # TODO: check whether this signal can be moved to save method of the model diff --git a/engine/apps/user_management/tests/test_alert_group_table_settings.py b/engine/apps/user_management/tests/test_alert_group_table_settings.py deleted file mode 100644 index a3583014f2..0000000000 --- a/engine/apps/user_management/tests/test_alert_group_table_settings.py +++ /dev/null @@ -1,88 +0,0 @@ -import pytest - -from apps.api.alert_group_table_columns import alert_group_table_user_settings -from apps.user_management.constants import AlertGroupTableColumnTypeChoices, default_columns - -DEFAULT_COLUMNS = default_columns() - - -@pytest.mark.parametrize( - "user_settings,organization_settings,expected_result", - [ - # user doesn't have settings, organization has default settings - all columns are visible - ( - None, - DEFAULT_COLUMNS, - {"visible": DEFAULT_COLUMNS, "hidden": []}, - ), - # user doesn't have settings, organization has updated settings - only default columns are visible - ( - None, - DEFAULT_COLUMNS + [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}], - { - "visible": DEFAULT_COLUMNS, - "hidden": [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}], - }, - ), - # user has settings, organization has default settings - only selected columns are visible - ( - DEFAULT_COLUMNS[:3], - DEFAULT_COLUMNS, - {"visible": DEFAULT_COLUMNS[:3], "hidden": DEFAULT_COLUMNS[3:]}, - ), - # user has settings, organization has unchanged settings - only selected columns are visible - ( - DEFAULT_COLUMNS[:3] - + [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}], - DEFAULT_COLUMNS + [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}], - { - "visible": ( - DEFAULT_COLUMNS[:3] - + [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}] - ), - "hidden": DEFAULT_COLUMNS[3:], - }, - ), - # user has settings, organization has updated settings - column was removed, remove from settings - ( - DEFAULT_COLUMNS[:3] - + [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}], - DEFAULT_COLUMNS, - {"visible": DEFAULT_COLUMNS[:3], "hidden": DEFAULT_COLUMNS[3:]}, - ), - # user has settings with reordered columns, organization has unchanged settings - selected columns in particular - # order are visible - ( - [ - DEFAULT_COLUMNS[1], - DEFAULT_COLUMNS[3], - {"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}, - DEFAULT_COLUMNS[2], - ], - DEFAULT_COLUMNS + [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}], - { - "visible": [ - DEFAULT_COLUMNS[1], - DEFAULT_COLUMNS[3], - {"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}, - DEFAULT_COLUMNS[2], - ], - "hidden": DEFAULT_COLUMNS[:1] + DEFAULT_COLUMNS[4:], - }, - ), - ], -) -@pytest.mark.django_db -def test_alert_group_table_user_settings( - user_settings, - organization_settings, - expected_result, - make_organization_and_user, -): - organization, user = make_organization_and_user() - organization.update_alert_group_table_columns(organization_settings) - if user_settings: - user.update_alert_group_table_columns_settings(user_settings) - result = alert_group_table_user_settings(user) - assert result == expected_result - assert user.alert_groups_table_selected_columns == result["visible"] From adc96f002fe2dc20a51d7fbf8f7e3438cd9ca696 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Tue, 28 Nov 2023 16:47:44 +0200 Subject: [PATCH 59/67] call reset on backend --- grafana-plugin/package.json | 2 +- grafana-plugin/src/assets/style/global.css | 5 ++-- .../ColumnsSelector/ColumnsSelector.tsx | 26 +++---------------- .../src/containers/Labels/Labels.tsx | 2 +- .../src/models/alertgroup/alertgroup.ts | 22 ++++++++++------ .../src/models/alertgroup/alertgroup.types.ts | 2 +- .../src/pages/incidents/Incidents.tsx | 2 +- grafana-plugin/yarn.lock | 8 +++--- 8 files changed, 27 insertions(+), 42 deletions(-) diff --git a/grafana-plugin/package.json b/grafana-plugin/package.json index 9e20e5abc8..729b60baac 100644 --- a/grafana-plugin/package.json +++ b/grafana-plugin/package.json @@ -118,7 +118,7 @@ "@grafana/data": "^9.2.4", "@grafana/faro-web-sdk": "^1.0.0-beta4", "@grafana/faro-web-tracing": "^1.0.0-beta4", - "@grafana/labels": "~1.2.1", + "@grafana/labels": "~1.3.4", "@grafana/runtime": "9.3.0-beta1", "@grafana/ui": "^10.2.0", "@opentelemetry/api": "^1.3.0", diff --git a/grafana-plugin/src/assets/style/global.css b/grafana-plugin/src/assets/style/global.css index ef7ae8495e..5cc2b9ab41 100644 --- a/grafana-plugin/src/assets/style/global.css +++ b/grafana-plugin/src/assets/style/global.css @@ -44,9 +44,8 @@ margin-bottom: 16px; } -.rc-table-cell { - padding-left: 4px; - padding-right: 4px; +td.rc-table-cell { + height: 44px !important; /* works better than break-all, especially for table headers */ word-break: break-word; diff --git a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx index 056f14b74a..a88b26a8f3 100644 --- a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx +++ b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useRef } from 'react'; +import React, { useRef } from 'react'; import { DndContext, @@ -18,7 +18,6 @@ import { } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { Button, Checkbox, Icon, IconButton, LoadingPlaceholder, Tooltip, useStyles2 } from '@grafana/ui'; -import { isEqual } from 'lodash-es'; import { observer } from 'mobx-react'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; @@ -117,8 +116,6 @@ export const ColumnsSelector: React.FC = observer( .filter((col) => !col.isVisible) .sort((a, b) => a.id.toString().localeCompare(b.id.toString())); - const canResetData = useMemo(() => !isEqual(columns, getDefaultData()), [alertGroupStore.columns]); - const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { @@ -195,7 +192,7 @@ export const ColumnsSelector: React.FC = observer( variant={'secondary'} tooltipPlacement="top" tooltip={'Reset table to default columns'} - disabled={!canResetData || isResetLoading} + disabled={isResetLoading} onClick={WrapAutoLoadingState(onReset, ActionKey.RESET_COLUMNS_FROM_ALERT_GROUP)} > {isResetLoading ? : 'Reset'} @@ -210,27 +207,10 @@ export const ColumnsSelector: React.FC = observer( ); async function onReset() { - const columnsDefaultValues = getDefaultData(); - - await alertGroupStore.updateTableSettings(columnsDefaultValues, true); + await alertGroupStore.resetTableSettings(); await alertGroupStore.fetchTableSettings(); } - function getDefaultData() { - const { columns } = alertGroupStore; - - const columnsDefaultValues: { visible: AGColumn[]; hidden: AGColumn[] } = { - visible: columns - .filter((col) => col.type === AGColumnType.DEFAULT) - .sort((a, b) => (a.id as number) - (b.id as number)), - hidden: columns - .filter((col) => col.type === AGColumnType.LABEL) - .sort((a, b) => a.id.toString().localeCompare(b.id.toString())), - }; - - return columnsDefaultValues; - } - async function onItemChange(id: string | number) { const checkedItems = alertGroupStore.columns.filter((col) => col.isVisible); if (checkedItems.length === 1 && checkedItems[0].id === id) { diff --git a/grafana-plugin/src/containers/Labels/Labels.tsx b/grafana-plugin/src/containers/Labels/Labels.tsx index faa3e5d935..c30d9016ce 100644 --- a/grafana-plugin/src/containers/Labels/Labels.tsx +++ b/grafana-plugin/src/containers/Labels/Labels.tsx @@ -1,6 +1,6 @@ import React, { forwardRef, useCallback, useImperativeHandle, useState } from 'react'; -import ServiceLabels, { ServiceLabelsProps } from '@grafana/labels'; +import { ServiceLabels, ServiceLabelsProps } from '@grafana/labels'; import { Field } from '@grafana/ui'; import { isEmpty } from 'lodash-es'; import { observer } from 'mobx-react'; diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.ts b/grafana-plugin/src/models/alertgroup/alertgroup.ts index 473193739e..aadbad5cec 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.ts @@ -442,7 +442,7 @@ export class AlertGroupStore extends BaseStore { } @action - public async fetchTableSettings(): Promise { + async fetchTableSettings(): Promise { const tableSettings = await makeRequest('/alertgroup_table_settings', {}); const { hidden, visible } = tableSettings; @@ -455,7 +455,7 @@ export class AlertGroupStore extends BaseStore { @action @AutoLoadingState(ActionKey.ADD_NEW_COLUMN_TO_ALERT_GROUP) - public async updateTableSettings( + async updateTableSettings( columns: { visible: AGColumn[]; hidden: AGColumn[] }, isUserUpdate: boolean ): Promise { @@ -468,15 +468,21 @@ export class AlertGroupStore extends BaseStore { } @action - public async loadLabelsKeys(): Promise { - return await makeRequest(`/alertgroups/labels/keys/`, {}); + async resetTableSettings(): Promise { + return await makeRequest('/alertgroup_table_settings/reset', { method: 'POST' }).catch(() => + openErrorNotification('There was an error resetting the table settings') + ); + } + + @action + async loadLabelsKeys(): Promise { + return await makeRequest(`/alertgroups/labels/keys/`, {}).catch(() => + openErrorNotification('There was an error processing your request') + ); } @action - public async loadValuesForLabelKey( - key: LabelKey['id'], - search = '' - ): Promise<{ key: LabelKey; values: LabelValue[] }> { + async loadValuesForLabelKey(key: LabelKey['id'], search = ''): Promise<{ key: LabelKey; values: LabelValue[] }> { if (!key) { return { key: undefined, values: [] }; } diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.types.ts b/grafana-plugin/src/models/alertgroup/alertgroup.types.ts index b6e1f77ef7..80ca1c92a2 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.types.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.types.ts @@ -91,7 +91,7 @@ export interface Alert { } export interface AGColumn { - id: number | string; + id: string; name: string; isVisible: boolean; type?: AGColumnType; diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index 54efde5c9a..fa0822850d 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -812,7 +812,7 @@ class Incidents extends React.Component return { width: isHorizontalScrolling ? '200px' : '10%', title: capitalize(column.name), - key: column.id.toString(), + key: column.id, render: (item: AlertType) => this.renderCustomColumn(column, item), }; }); diff --git a/grafana-plugin/yarn.lock b/grafana-plugin/yarn.lock index ab95dc955c..b7f72dbb52 100644 --- a/grafana-plugin/yarn.lock +++ b/grafana-plugin/yarn.lock @@ -2020,10 +2020,10 @@ "@opentelemetry/sdk-trace-web" "^1.8.0" "@opentelemetry/semantic-conventions" "^1.8.0" -"@grafana/labels@~1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@grafana/labels/-/labels-1.2.1.tgz#4113d584bf5cd826d011f957cb69c90bd0416ea8" - integrity sha512-Nlqqvjwh0MjWsqnfpYbKdYwByeKSmEpiit5mKd6Mnnbc5Hxb8ORIruMr40lTxxWLEnDfhENcAs6pvlBuIMG7tQ== +"@grafana/labels@~1.3.4": + version "1.3.4" + resolved "https://registry.yarnpkg.com/@grafana/labels/-/labels-1.3.4.tgz#8d9cdd215a80a1da1045d402c037be85d7efd6f5" + integrity sha512-YYCuLGvtrMz7KkbMc6qoNJQr6drDLo6mMI27LcqsTDMHCNO3uJWpzC1Q2Y9MIwctIuTFYhbgfLvIunEegCx6PQ== dependencies: "@emotion/css" "^11.11.2" "@grafana/ui" "^10.0.0" From 43ee175ea19cbebb816f8740e378bac86407362e Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 28 Nov 2023 15:54:41 +0100 Subject: [PATCH 60/67] Add flag to columns endpoint to show if user has default settings --- engine/apps/api/alert_group_table_columns.py | 14 ++++++++++---- .../api/tests/test_alert_group_table_settings.py | 12 ++++++++---- engine/apps/user_management/constants.py | 1 + 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/engine/apps/api/alert_group_table_columns.py b/engine/apps/api/alert_group_table_columns.py index 056d33b11a..8fb3381109 100644 --- a/engine/apps/api/alert_group_table_columns.py +++ b/engine/apps/api/alert_group_table_columns.py @@ -8,18 +8,24 @@ def alert_group_table_user_settings(user: "User") -> AlertGroupTableColumns: """ - Returns user settings for alert group table columns. + Returns user settings for alert group table columns. The flag "default" shows that user has default settings for + visible columns. It's used by frontend to enable/disable `reset` button. This function uses lazy update to update columns settings for organization and for user. """ + default_organization_columns = default_columns() if not user.organization.alert_group_table_columns: - user.organization.update_alert_group_table_columns(default_columns()) + user.organization.update_alert_group_table_columns(default_organization_columns) organization_columns = user.organization.alert_group_table_columns if user.alert_group_table_selected_columns: visible_columns = [ column for column in user.alert_group_table_selected_columns if column in organization_columns ] else: - visible_columns = default_columns() + visible_columns = default_organization_columns user.update_alert_group_table_selected_columns(visible_columns) hidden_columns = [column for column in organization_columns if column not in visible_columns] - return {"visible": visible_columns, "hidden": hidden_columns} + return { + "visible": visible_columns, + "hidden": hidden_columns, + "default": visible_columns == default_organization_columns, + } diff --git a/engine/apps/api/tests/test_alert_group_table_settings.py b/engine/apps/api/tests/test_alert_group_table_settings.py index 092c319a56..175f3308d5 100644 --- a/engine/apps/api/tests/test_alert_group_table_settings.py +++ b/engine/apps/api/tests/test_alert_group_table_settings.py @@ -11,7 +11,7 @@ def columns_settings(add_column=None): - default_settings = {"visible": DEFAULT_COLUMNS[:], "hidden": []} + default_settings = {"visible": DEFAULT_COLUMNS[:], "hidden": [], "default": True} if add_column: default_settings["hidden"].append(add_column) return default_settings @@ -133,6 +133,7 @@ def test_update_columns_settings( response = client.put(url, data=updated_columns_settings, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status_code if status_code == status.HTTP_200_OK: + updated_columns_settings["default"] = updated_columns_settings["visible"] == DEFAULT_COLUMNS assert response.json() == updated_columns_settings @@ -260,7 +261,7 @@ def test_reset_user_columns_permissions( ( None, DEFAULT_COLUMNS, - {"visible": DEFAULT_COLUMNS, "hidden": []}, + {"visible": DEFAULT_COLUMNS, "hidden": [], "default": True}, ), # user doesn't have settings, organization has updated settings - only default columns are visible ( @@ -269,13 +270,14 @@ def test_reset_user_columns_permissions( { "visible": DEFAULT_COLUMNS, "hidden": [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}], + "default": True, }, ), # user has settings, organization has default settings - only selected columns are visible ( DEFAULT_COLUMNS[:3], DEFAULT_COLUMNS, - {"visible": DEFAULT_COLUMNS[:3], "hidden": DEFAULT_COLUMNS[3:]}, + {"visible": DEFAULT_COLUMNS[:3], "hidden": DEFAULT_COLUMNS[3:], "default": False}, ), # user has settings, organization has unchanged settings - only selected columns are visible ( @@ -288,6 +290,7 @@ def test_reset_user_columns_permissions( + [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}] ), "hidden": DEFAULT_COLUMNS[3:], + "default": False, }, ), # user has settings, organization has updated settings - column was removed, remove from settings @@ -295,7 +298,7 @@ def test_reset_user_columns_permissions( DEFAULT_COLUMNS[:3] + [{"name": "Test", "id": "test", "type": AlertGroupTableColumnTypeChoices.LABEL.value}], DEFAULT_COLUMNS, - {"visible": DEFAULT_COLUMNS[:3], "hidden": DEFAULT_COLUMNS[3:]}, + {"visible": DEFAULT_COLUMNS[:3], "hidden": DEFAULT_COLUMNS[3:], "default": False}, ), # user has settings with reordered columns, organization has unchanged settings - selected columns in particular # order are visible @@ -315,6 +318,7 @@ def test_reset_user_columns_permissions( DEFAULT_COLUMNS[2], ], "hidden": DEFAULT_COLUMNS[:1] + DEFAULT_COLUMNS[4:], + "default": False, }, ), ], diff --git a/engine/apps/user_management/constants.py b/engine/apps/user_management/constants.py index 58aeaac846..16c3b4ff76 100644 --- a/engine/apps/user_management/constants.py +++ b/engine/apps/user_management/constants.py @@ -29,6 +29,7 @@ class AlertGroupTableColumn(typing.TypedDict): class AlertGroupTableColumns(typing.TypedDict): visible: typing.List[AlertGroupTableColumn] hidden: typing.List[AlertGroupTableColumn] + default: bool def default_columns() -> typing.List[AlertGroupTableColumn]: From 61532b9f37236684937dee2350ac98a3da7bcac3 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Wed, 29 Nov 2023 10:33:41 +0200 Subject: [PATCH 61/67] use default flag from backend --- .../ColumnsSelector/ColumnsSelector.tsx | 4 ++-- .../src/models/alertgroup/alertgroup.ts | 10 +++++++-- .../src/pages/incidents/Incidents.tsx | 21 +++++++++++++++---- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx index a88b26a8f3..d96ab1abd4 100644 --- a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx +++ b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx @@ -109,7 +109,7 @@ export const ColumnsSelector: React.FC = observer( const styles = useStyles2(getColumnsSelectorStyles); - const { columns } = alertGroupStore; + const { columns, isDefaultColumnOrder } = alertGroupStore; const visibleColumns = columns.filter((col) => col.isVisible); const hiddenColumns = columns @@ -192,7 +192,7 @@ export const ColumnsSelector: React.FC = observer( variant={'secondary'} tooltipPlacement="top" tooltip={'Reset table to default columns'} - disabled={isResetLoading} + disabled={isResetLoading || isDefaultColumnOrder} onClick={WrapAutoLoadingState(onReset, ActionKey.RESET_COLUMNS_FROM_ALERT_GROUP)} > {isResetLoading ? : 'Reset'} diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.ts b/grafana-plugin/src/models/alertgroup/alertgroup.ts index aadbad5cec..054ca42a24 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.ts @@ -77,6 +77,9 @@ export class AlertGroupStore extends BaseStore { @observable columns: AGColumn[] = []; + @observable + isDefaultColumnOrder = false; + constructor(rootStore: RootStore) { super(rootStore); @@ -445,8 +448,9 @@ export class AlertGroupStore extends BaseStore { async fetchTableSettings(): Promise { const tableSettings = await makeRequest('/alertgroup_table_settings', {}); - const { hidden, visible } = tableSettings; + const { hidden, visible, default: isDefaultOrder } = tableSettings; + this.isDefaultColumnOrder = isDefaultOrder; this.columns = [ ...visible.map((item: AGColumn): AGColumn => ({ ...item, isVisible: true })), ...hidden.map((item: AGColumn): AGColumn => ({ ...item, isVisible: false })), @@ -461,10 +465,12 @@ export class AlertGroupStore extends BaseStore { ): Promise { const method = isUserUpdate ? 'PUT' : 'POST'; - await makeRequest('/alertgroup_table_settings', { + const { default: isDefaultOrder } = await makeRequest('/alertgroup_table_settings', { method, data: { ...columns }, }); + + this.isDefaultColumnOrder = isDefaultOrder; } @action diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index fa0822850d..2ee227682d 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -633,16 +633,29 @@ class Incidents extends React.Component ); }; - renderStartedAt(alert: AlertType) { + renderStartedAt = (alert: AlertType) => { const m = moment(alert.started_at); + const { isHorizontalScrolling } = this.state; + + const date = m.format('MMM DD, YYYY'); + const time = m.format('HH:mm'); + + if (isHorizontalScrolling) { + // display date as 1 line + return ( + + {date} {time} + + ); + } return ( - {m.format('MMM DD, YYYY')} - {m.format('HH:mm')} + {date} + {time} ); - } + }; renderLabels = (item: AlertType) => { if (!item.labels.length) { From 4a32c8762d2736ac3d5581cdec01ae7607c6ced9 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Wed, 29 Nov 2023 10:43:29 +0200 Subject: [PATCH 62/67] after merge --- grafana-plugin/src/models/alertgroup/alertgroup.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.ts b/grafana-plugin/src/models/alertgroup/alertgroup.ts index e6a0736460..f962cd58b3 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.ts @@ -481,7 +481,10 @@ export class AlertGroupStore extends BaseStore { } @action - async loadValuesForLabelKey(key: LabelKey['id'], search = ''): Promise<{ key: LabelKey; values: LabelValue[] }> { + async loadValuesForLabelKey( + key: ApiSchemas['LabelKey']['id'], + search = '' + ): Promise<{ key: LabelKey; values: LabelValue[] }> { if (!key) { return { key: undefined, values: [] }; } From c0f22db773f41f4868ee487b12eef88b74218ad7 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Wed, 29 Nov 2023 10:44:53 +0200 Subject: [PATCH 63/67] build fix --- grafana-plugin/src/utils/decorators.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/grafana-plugin/src/utils/decorators.ts b/grafana-plugin/src/utils/decorators.ts index b20b95f674..83fcd756dc 100644 --- a/grafana-plugin/src/utils/decorators.ts +++ b/grafana-plugin/src/utils/decorators.ts @@ -1,28 +1,29 @@ import { openErrorNotification, openNotification, openWarningNotification } from 'utils'; import { LoaderStore } from 'models/loader/loader'; + export function AutoLoadingState(actionKey: string) { return function (_target: object, _key: string, descriptor: PropertyDescriptor) { - descriptor.value = async function (...args: any) { const originalFunction = descriptor.value; + descriptor.value = async function (...args: any) { LoaderStore.setLoadingAction(actionKey, true); try { - } finally { await originalFunction.apply(this, args); + } finally { LoaderStore.setLoadingAction(actionKey, false); } - }; }; + }; } export function WrapAutoLoadingState(callback: Function, actionKey: string): (...params: any[]) => Promise { - return async (...params) => { LoaderStore.setLoadingAction(actionKey, true); + try { await callback(...params); + } finally { LoaderStore.setLoadingAction(actionKey, false); } - } finally { }; } @@ -31,28 +32,28 @@ type GlobalNotificationConfig = { failure?: string; composeFailureMessageFn?: (error: unknown) => string; failureType?: 'error' | 'warning'; - }; + export function WithGlobalNotification({ success, failure, composeFailureMessageFn, -}: GlobalNotificationConfig) { failureType = 'error', +}: GlobalNotificationConfig) { return function (_target: object, _key: string, descriptor: PropertyDescriptor) { const childFunction = descriptor.value; - descriptor.value = async function (...args: any) { // eslint-disable-next-line @typescript-eslint/no-explicit-any + descriptor.value = async function (...args: any) { try { + const response = await childFunction.apply(this, args); openNotification(success); return response; - const response = await childFunction.apply(this, args); } catch (err) { - const message = composeFailureMessageFn ? composeFailureMessageFn(err) : failure; const open = failureType === 'error' ? openErrorNotification : openWarningNotification; + const message = composeFailureMessageFn ? composeFailureMessageFn(err) : failure; open(message); throw new Error(err); - }; } + }; }; } From 1621dd65c6e9b3a5c50afa436955d92cd89fe426 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Wed, 29 Nov 2023 10:58:09 +0200 Subject: [PATCH 64/67] build fix after using new typed auto generated interfaces --- .../containers/ColumnsSelectorWrapper/ColumnsModal.tsx | 9 +++++---- .../ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx | 4 ++-- grafana-plugin/src/models/alertgroup/alertgroup.ts | 9 +++++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx index e755c7139d..0bc073bcbf 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx @@ -18,7 +18,6 @@ import Block from 'components/GBlock/Block'; import Text from 'components/Text/Text'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; import { AGColumn, AGColumnType } from 'models/alertgroup/alertgroup.types'; -import { Label, LabelValue } from 'models/label/label.types'; import { ActionKey } from 'models/loader/action-keys'; import { useStore } from 'state/useStore'; import { openErrorNotification, pluralize } from 'utils'; @@ -26,15 +25,17 @@ import { UserActions } from 'utils/authorization'; import { useDebouncedCallback } from 'utils/hooks'; import { getColumnsSelectorWrapperStyles } from './ColumnsSelectorWrapper.styles'; +import { ApiSchemas } from 'network/oncall-api/api.types'; +import { components } from 'network/oncall-api/autogenerated-api.types'; interface ColumnsModalProps { isModalOpen: boolean; - labelKeys: Label[]; + labelKeys: ApiSchemas['LabelKey'][]; setIsModalOpen: (value: boolean) => void; inputRef: React.RefObject; } -interface SearchResult extends Label { +interface SearchResult extends Pick { isChecked: boolean; isCollapsed: boolean; values: any[]; @@ -142,7 +143,7 @@ export const ColumnsModal: React.FC = observer( ); - function renderLabelValues(keyName: string, values: LabelValue[]) { + function renderLabelValues(keyName: string, values: ApiSchemas['LabelValue'][]) { return ( {values.slice(0, 2).map((val) => ( diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx index b8c06dacdf..2d953925d1 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx @@ -8,13 +8,13 @@ import { ColumnsSelector, convertColumnsToTableSettings } from 'containers/Colum import { getColumnsSelectorWrapperStyles } from 'containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.styles'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; import { AGColumn } from 'models/alertgroup/alertgroup.types'; -import { Label } from 'models/label/label.types'; import { ActionKey } from 'models/loader/action-keys'; import { useStore } from 'state/useStore'; import { UserActions } from 'utils/authorization'; import { WrapAutoLoadingState } from 'utils/decorators'; import { ColumnsModal } from './ColumnsModal'; +import { ApiSchemas } from 'network/oncall-api/api.types'; interface ColumnsSelectorWrapperProps {} @@ -24,7 +24,7 @@ const ColumnsSelectorWrapper: React.FC = observer(( const [isColumnAddModalOpen, setIsColumnAddModalOpen] = useState(false); const [isFloatingDisplayOpen, setIsFloatingDisplayOpen] = useState(false); - const [labelKeys, setLabelKeys] = useState([]); + const [labelKeys, setLabelKeys] = useState([]); const inputRef = useRef(null); const wrappingFloatingContainerRef = useRef(null); diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.ts b/grafana-plugin/src/models/alertgroup/alertgroup.ts index f962cd58b3..b9b083970c 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.ts @@ -3,7 +3,6 @@ import qs from 'query-string'; import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; import BaseStore from 'models/base_store'; -import { Label, LabelKey, LabelValue } from 'models/label/label.types'; import { ActionKey } from 'models/loader/action-keys'; import { User } from 'models/user/user.types'; import { makeRequest } from 'network'; @@ -474,7 +473,7 @@ export class AlertGroupStore extends BaseStore { } @action - async loadLabelsKeys(): Promise { + async loadLabelsKeys(): Promise { return await makeRequest(`/alertgroups/labels/keys/`, {}).catch(() => openErrorNotification('There was an error processing your request') ); @@ -484,7 +483,7 @@ export class AlertGroupStore extends BaseStore { async loadValuesForLabelKey( key: ApiSchemas['LabelKey']['id'], search = '' - ): Promise<{ key: LabelKey; values: LabelValue[] }> { + ): Promise<{ key: ApiSchemas['LabelKey']; values: ApiSchemas['LabelValue'][] }> { if (!key) { return { key: undefined, values: [] }; } @@ -493,7 +492,9 @@ export class AlertGroupStore extends BaseStore { params: { search }, }); - const filteredValues = result.values.filter((v: LabelValue) => v.name.toLowerCase().includes(search.toLowerCase())); // TODO remove after backend search implementation + const filteredValues = result.values.filter((v: ApiSchemas['LabelValue']) => + v.name.toLowerCase().includes(search.toLowerCase()) + ); return { ...result, values: filteredValues }; } From 9d35c38bea1fac766c3db656217946a008e48096 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Wed, 29 Nov 2023 11:22:07 +0200 Subject: [PATCH 65/67] linter --- .../containers/ColumnsSelectorWrapper/ColumnsModal.tsx | 8 ++++---- .../ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx | 4 ++-- grafana-plugin/src/models/alertgroup/alertgroup.ts | 4 ++-- grafana-plugin/src/utils/decorators.ts | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx index 0bc073bcbf..3687c0f7cc 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx @@ -19,18 +19,18 @@ import Text from 'components/Text/Text'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; import { AGColumn, AGColumnType } from 'models/alertgroup/alertgroup.types'; import { ActionKey } from 'models/loader/action-keys'; +import { ApiSchemas } from 'network/oncall-api/api.types'; +import { components } from 'network/oncall-api/autogenerated-api.types'; import { useStore } from 'state/useStore'; import { openErrorNotification, pluralize } from 'utils'; import { UserActions } from 'utils/authorization'; import { useDebouncedCallback } from 'utils/hooks'; import { getColumnsSelectorWrapperStyles } from './ColumnsSelectorWrapper.styles'; -import { ApiSchemas } from 'network/oncall-api/api.types'; -import { components } from 'network/oncall-api/autogenerated-api.types'; interface ColumnsModalProps { isModalOpen: boolean; - labelKeys: ApiSchemas['LabelKey'][]; + labelKeys: Array; setIsModalOpen: (value: boolean) => void; inputRef: React.RefObject; } @@ -143,7 +143,7 @@ export const ColumnsModal: React.FC = observer( ); - function renderLabelValues(keyName: string, values: ApiSchemas['LabelValue'][]) { + function renderLabelValues(keyName: string, values: Array) { return ( {values.slice(0, 2).map((val) => ( diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx index 2d953925d1..9a3d5c5a1f 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx @@ -9,12 +9,12 @@ import { getColumnsSelectorWrapperStyles } from 'containers/ColumnsSelectorWrapp import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; import { AGColumn } from 'models/alertgroup/alertgroup.types'; import { ActionKey } from 'models/loader/action-keys'; +import { ApiSchemas } from 'network/oncall-api/api.types'; import { useStore } from 'state/useStore'; import { UserActions } from 'utils/authorization'; import { WrapAutoLoadingState } from 'utils/decorators'; import { ColumnsModal } from './ColumnsModal'; -import { ApiSchemas } from 'network/oncall-api/api.types'; interface ColumnsSelectorWrapperProps {} @@ -24,7 +24,7 @@ const ColumnsSelectorWrapper: React.FC = observer(( const [isColumnAddModalOpen, setIsColumnAddModalOpen] = useState(false); const [isFloatingDisplayOpen, setIsFloatingDisplayOpen] = useState(false); - const [labelKeys, setLabelKeys] = useState([]); + const [labelKeys, setLabelKeys] = useState>([]); const inputRef = useRef(null); const wrappingFloatingContainerRef = useRef(null); diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.ts b/grafana-plugin/src/models/alertgroup/alertgroup.ts index b9b083970c..834db7a528 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.ts @@ -473,7 +473,7 @@ export class AlertGroupStore extends BaseStore { } @action - async loadLabelsKeys(): Promise { + async loadLabelsKeys(): Promise> { return await makeRequest(`/alertgroups/labels/keys/`, {}).catch(() => openErrorNotification('There was an error processing your request') ); @@ -483,7 +483,7 @@ export class AlertGroupStore extends BaseStore { async loadValuesForLabelKey( key: ApiSchemas['LabelKey']['id'], search = '' - ): Promise<{ key: ApiSchemas['LabelKey']; values: ApiSchemas['LabelValue'][] }> { + ): Promise<{ key: ApiSchemas['LabelKey']; values: Array }> { if (!key) { return { key: undefined, values: [] }; } diff --git a/grafana-plugin/src/utils/decorators.ts b/grafana-plugin/src/utils/decorators.ts index 83fcd756dc..21f0c78e60 100644 --- a/grafana-plugin/src/utils/decorators.ts +++ b/grafana-plugin/src/utils/decorators.ts @@ -1,5 +1,5 @@ -import { openErrorNotification, openNotification, openWarningNotification } from 'utils'; import { LoaderStore } from 'models/loader/loader'; +import { openErrorNotification, openNotification, openWarningNotification } from 'utils'; export function AutoLoadingState(actionKey: string) { return function (_target: object, _key: string, descriptor: PropertyDescriptor) { From e2583d11eab992c1670df5dde421fe3b0041e23b Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Wed, 29 Nov 2023 11:37:23 +0200 Subject: [PATCH 66/67] labels version bump --- grafana-plugin/package.json | 2 +- grafana-plugin/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/grafana-plugin/package.json b/grafana-plugin/package.json index b82733ae2f..5219ad4ac7 100644 --- a/grafana-plugin/package.json +++ b/grafana-plugin/package.json @@ -122,7 +122,7 @@ "@grafana/data": "^9.2.4", "@grafana/faro-web-sdk": "^1.0.0-beta4", "@grafana/faro-web-tracing": "^1.0.0-beta4", - "@grafana/labels": "~1.3.4", + "@grafana/labels": "~1.3.5", "@grafana/runtime": "9.3.0-beta1", "@grafana/ui": "^10.2.0", "@opentelemetry/api": "^1.3.0", diff --git a/grafana-plugin/yarn.lock b/grafana-plugin/yarn.lock index de1d2d7bd1..c2da92671b 100644 --- a/grafana-plugin/yarn.lock +++ b/grafana-plugin/yarn.lock @@ -2027,10 +2027,10 @@ "@opentelemetry/sdk-trace-web" "^1.8.0" "@opentelemetry/semantic-conventions" "^1.8.0" -"@grafana/labels@~1.3.4": - version "1.3.4" - resolved "https://registry.yarnpkg.com/@grafana/labels/-/labels-1.3.4.tgz#8d9cdd215a80a1da1045d402c037be85d7efd6f5" - integrity sha512-YYCuLGvtrMz7KkbMc6qoNJQr6drDLo6mMI27LcqsTDMHCNO3uJWpzC1Q2Y9MIwctIuTFYhbgfLvIunEegCx6PQ== +"@grafana/labels@~1.3.5": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@grafana/labels/-/labels-1.3.5.tgz#c53b64e12a6360d7558dc9bc0fff8c6b31983acb" + integrity sha512-e79Ef/Bg5mGx0Mx6qGB65+6Z8HUHwXE4V8rjpI8EalWjARu6JlF27YBH28vbRX0kl1jepZHOi9EwYyck9y73PA== dependencies: "@emotion/css" "^11.11.2" "@grafana/ui" "^10.0.0" From 4a2f19751d588c2dc4b405a4f83d1483fe5ec11d Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Wed, 29 Nov 2023 14:04:57 +0200 Subject: [PATCH 67/67] pr feedback rename var --- .../ColumnsSelector/ColumnsSelector.tsx | 29 ++++++++++--------- .../ColumnsSelectorWrapper/ColumnsModal.tsx | 8 ++--- .../ColumnsSelectorWrapper.tsx | 6 ++-- .../src/models/alertgroup/alertgroup.ts | 10 +++---- .../src/models/alertgroup/alertgroup.types.ts | 6 ++-- .../src/pages/incidents/Incidents.tsx | 10 +++---- 6 files changed, 36 insertions(+), 33 deletions(-) diff --git a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx index d96ab1abd4..a8e9673fd0 100644 --- a/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx +++ b/grafana-plugin/src/containers/ColumnsSelector/ColumnsSelector.tsx @@ -24,7 +24,7 @@ import { CSSTransition, TransitionGroup } from 'react-transition-group'; import RenderConditionally from 'components/RenderConditionally/RenderConditionally'; import Text from 'components/Text/Text'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; -import { AGColumn, AGColumnType } from 'models/alertgroup/alertgroup.types'; +import { AlertGroupColumn, AlertGroupColumnType } from 'models/alertgroup/alertgroup.types'; import { ActionKey } from 'models/loader/action-keys'; import { useStore } from 'state/useStore'; import { openErrorNotification } from 'utils'; @@ -36,9 +36,9 @@ import { getColumnsSelectorStyles } from './ColumnsSelector.styles'; const TRANSITION_MS = 500; interface ColumnRowProps { - column: AGColumn; + column: AlertGroupColumn; onItemChange: (id: number | string) => void; - onColumnRemoval: (column: AGColumn) => void; + onColumnRemoval: (column: AlertGroupColumn) => void; } const ColumnRow: React.FC = ({ column, onItemChange, onColumnRemoval }) => { @@ -59,7 +59,7 @@ const ColumnRow: React.FC = ({ column, onItemChange, onColumnRem
    {column.name} - {column.type === AGColumnType.LABEL && ( + {column.type === AlertGroupColumnType.LABEL && ( @@ -75,7 +75,7 @@ const ColumnRow: React.FC = ({ column, onItemChange, onColumnRem /> - + = ({ column, onItemChange, onColumnRem interface ColumnsSelectorProps { onColumnAddModalOpen(): void; - onConfirmRemovalModalOpen(column: AGColumn): void; + onConfirmRemovalModalOpen(column: AlertGroupColumn): void; } export const ColumnsSelector: React.FC = observer( @@ -218,8 +218,8 @@ export const ColumnsSelector: React.FC = observer( return; } - alertGroupStore.columns = alertGroupStore.columns.map((item): AGColumn => { - let newItem: AGColumn = { ...item, isVisible: !item.isVisible }; + alertGroupStore.columns = alertGroupStore.columns.map((item): AlertGroupColumn => { + let newItem: AlertGroupColumn = { ...item, isVisible: !item.isVisible }; return item.id === id ? newItem : item; }); @@ -229,7 +229,7 @@ export const ColumnsSelector: React.FC = observer( function handleDragEnd(event: DragEndEvent, isVisible: boolean) { const { active, over } = event; - let searchableList: AGColumn[] = isVisible ? visibleColumns : hiddenColumns; + let searchableList: AlertGroupColumn[] = isVisible ? visibleColumns : hiddenColumns; if (active.id !== over.id) { const oldIndex = searchableList.findIndex((item) => item.id === active.id); @@ -244,10 +244,13 @@ export const ColumnsSelector: React.FC = observer( } ); -export function convertColumnsToTableSettings(columns: AGColumn[]): { visible: AGColumn[]; hidden: AGColumn[] } { - const tableSettings: { visible: AGColumn[]; hidden: AGColumn[] } = { - visible: columns.filter((col: AGColumn) => col.isVisible), - hidden: columns.filter((col: AGColumn) => !col.isVisible), +export function convertColumnsToTableSettings(columns: AlertGroupColumn[]): { + visible: AlertGroupColumn[]; + hidden: AlertGroupColumn[]; +} { + const tableSettings: { visible: AlertGroupColumn[]; hidden: AlertGroupColumn[] } = { + visible: columns.filter((col: AlertGroupColumn) => col.isVisible), + hidden: columns.filter((col: AlertGroupColumn) => !col.isVisible), }; return tableSettings; diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx index 3687c0f7cc..01df95efb0 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsModal.tsx @@ -17,7 +17,7 @@ import { observer } from 'mobx-react'; import Block from 'components/GBlock/Block'; import Text from 'components/Text/Text'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; -import { AGColumn, AGColumnType } from 'models/alertgroup/alertgroup.types'; +import { AlertGroupColumn, AlertGroupColumnType } from 'models/alertgroup/alertgroup.types'; import { ActionKey } from 'models/loader/action-keys'; import { ApiSchemas } from 'network/oncall-api/api.types'; import { components } from 'network/oncall-api/autogenerated-api.types'; @@ -184,16 +184,16 @@ export const ColumnsModal: React.FC = observer( ...searchResults .filter((item) => item.isChecked) .map( - (item): AGColumn => ({ + (item): AlertGroupColumn => ({ id: item.id, name: item.name, isVisible: false, - type: AGColumnType.LABEL, + type: AlertGroupColumnType.LABEL, }) ), ]; - const columns: { visible: AGColumn[]; hidden: AGColumn[] } = { + const columns: { visible: AlertGroupColumn[]; hidden: AlertGroupColumn[] } = { visible: mergedColumns.filter((col) => col.isVisible), hidden: mergedColumns.filter((col) => !col.isVisible), }; diff --git a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx index 9a3d5c5a1f..b4223bd205 100644 --- a/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx +++ b/grafana-plugin/src/containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.tsx @@ -7,7 +7,7 @@ import Text from 'components/Text/Text'; import { ColumnsSelector, convertColumnsToTableSettings } from 'containers/ColumnsSelector/ColumnsSelector'; import { getColumnsSelectorWrapperStyles } from 'containers/ColumnsSelectorWrapper/ColumnsSelectorWrapper.styles'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; -import { AGColumn } from 'models/alertgroup/alertgroup.types'; +import { AlertGroupColumn } from 'models/alertgroup/alertgroup.types'; import { ActionKey } from 'models/loader/action-keys'; import { ApiSchemas } from 'network/oncall-api/api.types'; import { useStore } from 'state/useStore'; @@ -20,7 +20,7 @@ interface ColumnsSelectorWrapperProps {} const ColumnsSelectorWrapper: React.FC = observer(() => { const [isConfirmRemovalModalOpen, setIsConfirmRemovalModalOpen] = useState(false); - const [columnToBeRemoved, setColumnToBeRemoved] = useState(undefined); + const [columnToBeRemoved, setColumnToBeRemoved] = useState(undefined); const [isColumnAddModalOpen, setIsColumnAddModalOpen] = useState(false); const [isFloatingDisplayOpen, setIsFloatingDisplayOpen] = useState(false); @@ -96,7 +96,7 @@ const ColumnsSelectorWrapper: React.FC = observer(( > setIsColumnAddModalOpen(!isColumnAddModalOpen)} - onConfirmRemovalModalOpen={(column: AGColumn) => { + onConfirmRemovalModalOpen={(column: AlertGroupColumn) => { setIsConfirmRemovalModalOpen(!isConfirmRemovalModalOpen); setColumnToBeRemoved(column); }} diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.ts b/grafana-plugin/src/models/alertgroup/alertgroup.ts index 834db7a528..93c2904961 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.ts @@ -14,7 +14,7 @@ import { openErrorNotification, refreshPageError, showApiError } from 'utils'; import LocationHelper from 'utils/LocationHelper'; import { AutoLoadingState } from 'utils/decorators'; -import { AGColumn, Alert, AlertAction, IncidentStatus } from './alertgroup.types'; +import { AlertGroupColumn, Alert, AlertAction, IncidentStatus } from './alertgroup.types'; export class AlertGroupStore extends BaseStore { @observable.shallow @@ -72,7 +72,7 @@ export class AlertGroupStore extends BaseStore { liveUpdatesPaused = false; @observable - columns: AGColumn[] = []; + columns: AlertGroupColumn[] = []; @observable isDefaultColumnOrder = false; @@ -444,15 +444,15 @@ export class AlertGroupStore extends BaseStore { this.isDefaultColumnOrder = isDefaultOrder; this.columns = [ - ...visible.map((item: AGColumn): AGColumn => ({ ...item, isVisible: true })), - ...hidden.map((item: AGColumn): AGColumn => ({ ...item, isVisible: false })), + ...visible.map((item: AlertGroupColumn): AlertGroupColumn => ({ ...item, isVisible: true })), + ...hidden.map((item: AlertGroupColumn): AlertGroupColumn => ({ ...item, isVisible: false })), ]; } @action @AutoLoadingState(ActionKey.ADD_NEW_COLUMN_TO_ALERT_GROUP) async updateTableSettings( - columns: { visible: AGColumn[]; hidden: AGColumn[] }, + columns: { visible: AlertGroupColumn[]; hidden: AlertGroupColumn[] }, isUserUpdate: boolean ): Promise { const method = isUserUpdate ? 'PUT' : 'POST'; diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.types.ts b/grafana-plugin/src/models/alertgroup/alertgroup.types.ts index 80ca1c92a2..7237424971 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.types.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.types.ts @@ -90,14 +90,14 @@ export interface Alert { undoAction?: AlertAction; } -export interface AGColumn { +export interface AlertGroupColumn { id: string; name: string; isVisible: boolean; - type?: AGColumnType; + type?: AlertGroupColumnType; } -export enum AGColumnType { +export enum AlertGroupColumnType { DEFAULT = 'default', LABEL = 'label', } diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index 2ee227682d..30d4790586 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -31,8 +31,8 @@ import { Alert as AlertType, AlertAction, IncidentStatus, - AGColumn, - AGColumnType, + AlertGroupColumn, + AlertGroupColumnType, } from 'models/alertgroup/alertgroup.types'; import { LabelKeyValue } from 'models/label/label.types'; import { renderRelatedUsers } from 'pages/incident/Incident.helpers'; @@ -719,7 +719,7 @@ class Incidents extends React.Component }; }; - renderCustomColumn = (column: AGColumn, alert: AlertType) => { + renderCustomColumn = (column: AlertGroupColumn, alert: AlertType) => { const matchingLabel = alert.labels?.find((label) => label.key.name === column.name)?.value.name; return ( @@ -817,8 +817,8 @@ class Incidents extends React.Component const mappedColumns: TableColumn[] = store.alertGroupStore.columns .filter((col) => col.isVisible) - .map((column: AGColumn): TableColumn => { - if (column.type === AGColumnType.DEFAULT && columnMapping[column.name]) { + .map((column: AlertGroupColumn): TableColumn => { + if (column.type === AlertGroupColumnType.DEFAULT && columnMapping[column.name]) { return columnMapping[column.name]; }