From c2ffd286751eaae7fe5332ffd3e5059d8d352094 Mon Sep 17 00:00:00 2001 From: Dominik Broj Date: Mon, 26 Feb 2024 14:52:26 +0100 Subject: [PATCH] Snow outgoing webhooks tab (#3918) # What this PR does - implement visual part of ServiceNow Outgoing Tab - reuse created components on outgoing webhooks page - make yarn:lint:fix to remove unused imports - fix live reload during development - remove unused babel dependencies ## Which issue(s) this PR fixes - https://github.com/grafana/oncall/issues/3408 - partially https://github.com/grafana/oncall-private/issues/2462 ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required) --- grafana-plugin/.eslintrc.js | 6 +- .../integrations/maintenanceMode.test.ts | 4 +- .../createAdvancedWebhook.test.ts | 2 +- .../createSimpleWebhook.test.ts | 2 +- grafana-plugin/package.json | 20 +- .../CopyToClipboardIcon.tsx | 25 + .../HamburgerContextMenu.tsx | 77 ++ .../HamburgerMenuIcon.module.scss} | 0 .../HamburgerMenuIcon.tsx} | 6 +- .../IntegrationCollapsibleTreeView.tsx | 7 +- .../IntegrationContactPoint.tsx | 9 +- .../IntegrationHowToConnect.tsx | 9 +- .../IntegrationInputField.tsx | 13 +- .../IntegrationLogo/IntegrationLogo.tsx | 2 +- .../IntegrationLogoWithTitle.tsx | 20 + .../Integrations/IntegrationBlock.tsx | 4 +- .../Integrations/IntegrationTag.tsx | 37 + .../components/MonacoEditor/MonacoEditor.tsx | 10 +- .../src/components/SourceCode/SourceCode.tsx | 36 +- grafana-plugin/src/components/Tabs/Tabs.tsx | 43 +- .../src/components/Tag/Tag.module.css | 6 + grafana-plugin/src/components/Tag/Tag.tsx | 10 +- .../Webhooks/WebhookLastEventDetails.tsx | 129 +++ .../Webhooks/WebhookLastEventTimestamp.tsx | 72 ++ .../src/components/Webhooks/WebhookName.tsx | 43 + .../Webhooks/WebhookStatusCodeBadge.tsx | 31 + .../ExpandedIntegrationRouteDisplay.tsx | 4 +- .../OutgoingWebhookForm.config.tsx | 86 +- .../OutgoingWebhookForm.module.css | 3 +- .../OutgoingWebhookForm.tsx | 64 +- .../OutgoingWebhookStatus.tsx | 117 +-- .../WebhooksTemplateEditor.tsx | 10 +- .../alert_receive_channel.ts | 6 +- .../outgoing_webhook.types.ts | 79 ++ .../pages/integration/Integration.module.scss | 7 - .../src/pages/integration/Integration.tsx | 309 +++--- .../OutgoingTab/ConnectIntegrationModal.tsx | 29 + .../ConnectedIntegrationsTable.tsx | 73 ++ .../NewOutgoingWebhookDrawerContent.tsx | 46 + .../OutgoingTab/OtherIntegrations.tsx | 31 + .../OutgoingTab/OutgoingTab.styles.ts | 77 ++ .../integration/OutgoingTab/OutgoingTab.tsx | 114 +++ .../OutgoingTab/OutgoingTab.types.ts | 23 + .../OutgoingWebhookDetailsDrawerTabs.tsx | 111 +++ .../OutgoingTab/OutgoingWebhookFormFields.tsx | 230 +++++ .../OutgoingTab/OutgoingWebhooksTable.tsx | 153 +++ .../src/pages/integrations/Integrations.tsx | 4 +- .../OutgoingWebhooks.module.scss | 28 +- .../outgoing_webhooks/OutgoingWebhooks.tsx | 226 ++--- .../src/plugin/GrafanaPluginRootPage.tsx | 1 - grafana-plugin/src/utils/LocationHelper.ts | 4 + grafana-plugin/src/utils/hooks.tsx | 56 +- grafana-plugin/src/utils/string.ts | 9 + grafana-plugin/src/utils/styles.ts | 9 + grafana-plugin/webpack.config.ts | 5 +- grafana-plugin/yarn.lock | 908 +----------------- 56 files changed, 1913 insertions(+), 1532 deletions(-) create mode 100644 grafana-plugin/src/components/CopyToClipboardIcon/CopyToClipboardIcon.tsx create mode 100644 grafana-plugin/src/components/HamburgerContextMenu/HamburgerContextMenu.tsx rename grafana-plugin/src/components/{HamburgerMenu/HamburgerMenu.module.scss => HamburgerMenuIcon/HamburgerMenuIcon.module.scss} (100%) rename grafana-plugin/src/components/{HamburgerMenu/HamburgerMenu.tsx => HamburgerMenuIcon/HamburgerMenuIcon.tsx} (85%) create mode 100644 grafana-plugin/src/components/IntegrationLogo/IntegrationLogoWithTitle.tsx create mode 100644 grafana-plugin/src/components/Integrations/IntegrationTag.tsx create mode 100644 grafana-plugin/src/components/Webhooks/WebhookLastEventDetails.tsx create mode 100644 grafana-plugin/src/components/Webhooks/WebhookLastEventTimestamp.tsx create mode 100644 grafana-plugin/src/components/Webhooks/WebhookName.tsx create mode 100644 grafana-plugin/src/components/Webhooks/WebhookStatusCodeBadge.tsx create mode 100644 grafana-plugin/src/pages/integration/OutgoingTab/ConnectIntegrationModal.tsx create mode 100644 grafana-plugin/src/pages/integration/OutgoingTab/ConnectedIntegrationsTable.tsx create mode 100644 grafana-plugin/src/pages/integration/OutgoingTab/NewOutgoingWebhookDrawerContent.tsx create mode 100644 grafana-plugin/src/pages/integration/OutgoingTab/OtherIntegrations.tsx create mode 100644 grafana-plugin/src/pages/integration/OutgoingTab/OutgoingTab.styles.ts create mode 100644 grafana-plugin/src/pages/integration/OutgoingTab/OutgoingTab.tsx create mode 100644 grafana-plugin/src/pages/integration/OutgoingTab/OutgoingTab.types.ts create mode 100644 grafana-plugin/src/pages/integration/OutgoingTab/OutgoingWebhookDetailsDrawerTabs.tsx create mode 100644 grafana-plugin/src/pages/integration/OutgoingTab/OutgoingWebhookFormFields.tsx create mode 100644 grafana-plugin/src/pages/integration/OutgoingTab/OutgoingWebhooksTable.tsx create mode 100644 grafana-plugin/src/utils/styles.ts diff --git a/grafana-plugin/.eslintrc.js b/grafana-plugin/.eslintrc.js index 1baa2f59..246d9623 100644 --- a/grafana-plugin/.eslintrc.js +++ b/grafana-plugin/.eslintrc.js @@ -3,7 +3,7 @@ rulesDirPlugin.RULES_DIR = 'tools/eslint-rules'; module.exports = { extends: ['./.config/.eslintrc'], - plugins: ['rulesdir', 'import'], + plugins: ['rulesdir', 'import', 'unused-imports'], settings: { 'import/internal-regex': '^assets|^components|^containers|^contexts|^icons|^models|^network|^pages|^services|^state|^utils|^plugin', @@ -37,7 +37,9 @@ module.exports = { }, ], 'no-console': ['warn', { allow: ['warn', 'error'] }], - 'no-unused-vars': [ + 'no-unused-vars': 'off', + 'unused-imports/no-unused-imports': ['warn'], + 'unused-imports/no-unused-vars': [ 'warn', { vars: 'all', diff --git a/grafana-plugin/e2e-tests/integrations/maintenanceMode.test.ts b/grafana-plugin/e2e-tests/integrations/maintenanceMode.test.ts index d1f61be7..88d0c76c 100644 --- a/grafana-plugin/e2e-tests/integrations/maintenanceMode.test.ts +++ b/grafana-plugin/e2e-tests/integrations/maintenanceMode.test.ts @@ -40,7 +40,7 @@ test.describe('maintenance mode works', () => { const enableMaintenanceMode = async (page: Page, mode: MaintenanceModeType): Promise => { await _openIntegrationSettingsPopup(page, true); // open the maintenance mode settings drawer + fill in the maintenance details - await page.getByTestId('integration-start-maintenance').click(); + await page.getByText('Start Maintenance').click(); // fill in the form const maintenanceModeDrawer = page.getByTestId('maintenance-mode-drawer'); @@ -78,7 +78,7 @@ test.describe('maintenance mode works', () => { await _openIntegrationSettingsPopup(page, true); // click the stop maintenance button - await page.getByTestId('integration-stop-maintenance').click(); + await page.getByText('Stop Maintenance').click(); // in the modal popup, confirm that we want to stop it await clickButton({ diff --git a/grafana-plugin/e2e-tests/outgoingWebhooks/createAdvancedWebhook.test.ts b/grafana-plugin/e2e-tests/outgoingWebhooks/createAdvancedWebhook.test.ts index ac7e6e39..713cf705 100644 --- a/grafana-plugin/e2e-tests/outgoingWebhooks/createAdvancedWebhook.test.ts +++ b/grafana-plugin/e2e-tests/outgoingWebhooks/createAdvancedWebhook.test.ts @@ -40,7 +40,7 @@ test('create advanced webhook and check it is displayed on the list correctly', await webhooksFormDivs.locator('.monaco-editor').first().click(); await page.keyboard.insertText(WEBHOOK_URL); - await clickButton({ page, buttonText: 'Create Webhook' }); + await clickButton({ page, buttonText: 'Create' }); await checkWebhookPresenceInTable({ page, webhookName: WEBHOOK_NAME, expectedTriggerType: 'Resolved' }); }); diff --git a/grafana-plugin/e2e-tests/outgoingWebhooks/createSimpleWebhook.test.ts b/grafana-plugin/e2e-tests/outgoingWebhooks/createSimpleWebhook.test.ts index 9ecaf8f5..6a4d2982 100644 --- a/grafana-plugin/e2e-tests/outgoingWebhooks/createSimpleWebhook.test.ts +++ b/grafana-plugin/e2e-tests/outgoingWebhooks/createSimpleWebhook.test.ts @@ -18,7 +18,7 @@ test('create simple webhook and check it is displayed on the list correctly', as await page.locator('[name=name]').fill(WEBHOOK_NAME); await page.getByLabel('New Outgoing Webhook').getByRole('img').nth(1).click(); // Open team dropdown await page.getByLabel('Select options menu').getByText('No team').click(); - await clickButton({ page, buttonText: 'Create Webhook' }); + await clickButton({ page, buttonText: 'Create' }); await checkWebhookPresenceInTable({ page, webhookName: WEBHOOK_NAME, expectedTriggerType: 'Escalation step' }); }); diff --git a/grafana-plugin/package.json b/grafana-plugin/package.json index 8716fc34..727c440b 100644 --- a/grafana-plugin/package.json +++ b/grafana-plugin/package.json @@ -4,7 +4,7 @@ "description": "Grafana OnCall Plugin", "scripts": { "lint": "eslint --cache --ext .js,.jsx,.ts,.tsx --max-warnings=0 ./src ./e2e-tests", - "lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --quiet ./src ./e2e-tests", + "lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx ./src ./e2e-tests", "stylelint": "stylelint ./src/**/*.{css,scss,module.css,module.scss}", "stylelint:fix": "stylelint --fix ./src/**/*.{css,scss,module.css,module.scss}", "build": "webpack -c ./webpack.config.ts --env production", @@ -19,7 +19,6 @@ "test:e2e:gen": "yarn playwright codegen http://localhost:3000", "e2e-show-report": "yarn playwright show-report", "generate-types": "cd ./src/network/oncall-api/types-generator && yarn generate", - "dev": "webpack -c ./webpack.config.ts --env development", "watch": "webpack -w -c ./webpack.config.ts --env development", "sign": "npx --yes @grafana/sign-plugin@latest", "start": "yarn watch", @@ -47,21 +46,6 @@ "author": "Grafana Labs", "license": "Apache-2.0", "devDependencies": { - "@babel/core": "^7.21.4", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-decorators": "^7.20.0", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", - "@babel/plugin-proposal-object-rest-spread": "^7.18.9", - "@babel/plugin-proposal-optional-chaining": "^7.18.9", - "@babel/plugin-syntax-decorators": "^7.18.6", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-transform-destructuring": "^7.20.0", - "@babel/plugin-transform-react-constant-elements": "^7.18.12", - "@babel/plugin-transform-runtime": "^7.19.6", - "@babel/plugin-transform-typescript": "^7.18.12", - "@babel/preset-env": "^7.18.10", - "@babel/preset-react": "^7.18.6", - "@babel/preset-typescript": "^7.18.6", "@grafana/eslint-config": "^6.0.0", "@grafana/tsconfig": "^1.2.0-rc1", "@jest/globals": "^27.5.1", @@ -87,7 +71,6 @@ "@types/testing-library__jest-dom": "5.14.8", "@types/throttle-debounce": "^5.0.0", "@typescript-eslint/eslint-plugin": "^5.40.1", - "babel-plugin-dynamic-import-node": "^2.3.3", "copy-webpack-plugin": "^11.0.0", "css-loader": "^6.7.3", "dompurify": "^2.3.12", @@ -98,6 +81,7 @@ "eslint-plugin-react": "^7.31.10", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-rulesdir": "^0.2.1", + "eslint-plugin-unused-imports": "^3.1.0", "eslint-webpack-plugin": "^4.0.1", "fork-ts-checker-webpack-plugin": "^8.0.0", "glob": "^10.2.7", diff --git a/grafana-plugin/src/components/CopyToClipboardIcon/CopyToClipboardIcon.tsx b/grafana-plugin/src/components/CopyToClipboardIcon/CopyToClipboardIcon.tsx new file mode 100644 index 00000000..1202de2a --- /dev/null +++ b/grafana-plugin/src/components/CopyToClipboardIcon/CopyToClipboardIcon.tsx @@ -0,0 +1,25 @@ +import React, { FC } from 'react'; + +import { IconButton } from '@grafana/ui'; +import CopyToClipboard from 'react-copy-to-clipboard'; + +import { openNotification } from 'utils/utils'; + +interface CopyToClipboardProps { + text: string; + iconButtonProps?: Partial[0]>; +} + +const CopyToClipboardIcon: FC = ({ text, iconButtonProps }) => { + const onCopy = () => { + openNotification('Copied to clipboard'); + }; + + return ( + + + + ); +}; + +export default CopyToClipboardIcon; diff --git a/grafana-plugin/src/components/HamburgerContextMenu/HamburgerContextMenu.tsx b/grafana-plugin/src/components/HamburgerContextMenu/HamburgerContextMenu.tsx new file mode 100644 index 00000000..bd84f8cf --- /dev/null +++ b/grafana-plugin/src/components/HamburgerContextMenu/HamburgerContextMenu.tsx @@ -0,0 +1,77 @@ +import React, { FC, ReactNode } from 'react'; + +import { css } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data'; +import { useStyles2 } from '@grafana/ui'; + +import { HamburgerMenuIcon } from 'components/HamburgerMenuIcon/HamburgerMenuIcon'; +import { WithContextMenu } from 'components/WithContextMenu/WithContextMenu'; +import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; +import { isUserActionAllowed, UserAction } from 'utils/authorization/authorization'; + +interface HamburgerContextMenuProps { + items: Array< + { onClick?: () => void; label: ReactNode; requiredPermission?: UserAction; hidden?: boolean } | 'divider' + >; + hamburgerIconClassName?: string; +} + +export const HamburgerContextMenu: FC = ({ items, hamburgerIconClassName }) => { + const styles = useStyles2(getStyles); + + return ( + ( +
+ {items.map((item, idx) => { + if (item === 'divider') { + return
; + } else if (item.hidden) { + return null; + } + + return item.requiredPermission ? ( + +
+ {item.label} +
+
+ ) : ( +
+ {item.label} +
+ ); + })} +
+ )} + > + {({ openMenu }) => ( + + )} + + ); +}; + +export const getStyles = (theme: GrafanaTheme2) => ({ + menuList: css({ + display: 'flex', + flexDirection: 'column', + width: '225px', + borderRadius: '2px', + }), + menuItem: css({ + padding: '8px', + whiteSpace: 'nowrap', + borderLeft: '2px solid transparent', + minWidth: '84px', + gap: '8px', + cursor: 'pointer', + '&:hover': { + background: theme.colors.background.secondary, + }, + }), +}); diff --git a/grafana-plugin/src/components/HamburgerMenu/HamburgerMenu.module.scss b/grafana-plugin/src/components/HamburgerMenuIcon/HamburgerMenuIcon.module.scss similarity index 100% rename from grafana-plugin/src/components/HamburgerMenu/HamburgerMenu.module.scss rename to grafana-plugin/src/components/HamburgerMenuIcon/HamburgerMenuIcon.module.scss diff --git a/grafana-plugin/src/components/HamburgerMenu/HamburgerMenu.tsx b/grafana-plugin/src/components/HamburgerMenuIcon/HamburgerMenuIcon.tsx similarity index 85% rename from grafana-plugin/src/components/HamburgerMenu/HamburgerMenu.tsx rename to grafana-plugin/src/components/HamburgerMenuIcon/HamburgerMenuIcon.tsx index 43b4ed88..51644a78 100644 --- a/grafana-plugin/src/components/HamburgerMenu/HamburgerMenu.tsx +++ b/grafana-plugin/src/components/HamburgerMenuIcon/HamburgerMenuIcon.tsx @@ -3,9 +3,9 @@ import React, { useRef } from 'react'; import { Icon } from '@grafana/ui'; import cn from 'classnames/bind'; -import styles from './HamburgerMenu.module.scss'; +import styles from './HamburgerMenuIcon.module.scss'; -interface HamburgerMenuProps { +interface HamburgerMenuIconProps { openMenu: React.MouseEventHandler; listWidth: number; listBorder: number; @@ -16,7 +16,7 @@ interface HamburgerMenuProps { const cx = cn.bind(styles); -export const HamburgerMenu: React.FC = (props) => { +export const HamburgerMenuIcon: React.FC = (props) => { const ref = useRef(); const { openMenu, listBorder, listWidth, withBackground, className, stopPropagation = false } = props; return ( diff --git a/grafana-plugin/src/components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView.tsx b/grafana-plugin/src/components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView.tsx index b2cda6c7..a1f70f25 100644 --- a/grafana-plugin/src/components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView.tsx +++ b/grafana-plugin/src/components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView.tsx @@ -14,12 +14,11 @@ const cx = cn.bind(styles); export interface IntegrationCollapsibleItem { isHidden?: boolean; customIcon?: IconName; - canHoverIcon: boolean; + canHoverIcon?: boolean; isTextIcon?: boolean; - collapsedView: (toggle?: () => void) => React.ReactNode; // needs toggle param for toggling on click + collapsedView?: (toggle?: () => void) => React.ReactNode; // needs toggle param for toggling on click expandedView: () => React.ReactNode; // for consistency, this is also a function - isCollapsible: boolean; - iconText?: string; + isCollapsible?: boolean; isExpanded?: boolean; startingElemPosition?: string; onStateChange?(isChecked: boolean): void; diff --git a/grafana-plugin/src/components/IntegrationContactPoint/IntegrationContactPoint.tsx b/grafana-plugin/src/components/IntegrationContactPoint/IntegrationContactPoint.tsx index 7a127489..a2291d16 100644 --- a/grafana-plugin/src/components/IntegrationContactPoint/IntegrationContactPoint.tsx +++ b/grafana-plugin/src/components/IntegrationContactPoint/IntegrationContactPoint.tsx @@ -18,7 +18,7 @@ import { observer } from 'mobx-react'; import { GTable } from 'components/GTable/GTable'; import { IntegrationBlock } from 'components/Integrations/IntegrationBlock'; -import { Tag } from 'components/Tag/Tag'; +import IntegrationTag from 'components/Integrations/IntegrationTag'; import { Text } from 'components/Text/Text'; import { WithConfirm } from 'components/WithConfirm/WithConfirm'; import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers'; @@ -26,7 +26,6 @@ import { ContactPoint } from 'models/alert_receive_channel/alert_receive_channel import { ApiSchemas } from 'network/oncall-api/api.types'; import styles from 'pages/integration/Integration.module.scss'; import { useStore } from 'state/useStore'; -import { getVar } from 'utils/DOM'; import { openErrorNotification, openNotification } from 'utils/utils'; const cx = cn.bind(styles); @@ -145,11 +144,7 @@ export const IntegrationContactPoint: React.FC<{ )} - - - Contact point - - + Contact point {contactPoints?.length ? ( diff --git a/grafana-plugin/src/components/IntegrationHowToConnect/IntegrationHowToConnect.tsx b/grafana-plugin/src/components/IntegrationHowToConnect/IntegrationHowToConnect.tsx index e4db7b57..1f9cedc3 100644 --- a/grafana-plugin/src/components/IntegrationHowToConnect/IntegrationHowToConnect.tsx +++ b/grafana-plugin/src/components/IntegrationHowToConnect/IntegrationHowToConnect.tsx @@ -6,12 +6,11 @@ import { noop } from 'lodash-es'; import { IntegrationInputField } from 'components/IntegrationInputField/IntegrationInputField'; import { IntegrationBlock } from 'components/Integrations/IntegrationBlock'; -import { Tag } from 'components/Tag/Tag'; +import { IntegrationTag } from 'components/Integrations/IntegrationTag'; import { Text } from 'components/Text/Text'; import { ApiSchemas } from 'network/oncall-api/api.types'; import styles from 'pages/integration/Integration.module.scss'; import { useStore } from 'state/useStore'; -import { getVar } from 'utils/DOM'; const cx = cn.bind(styles); @@ -40,11 +39,7 @@ export const IntegrationHowToConnect: React.FC<{ id: ApiSchemas['AlertReceiveCha toggle={noop} heading={
- - - {howToConnectTagName(item?.integration)} - - + {howToConnectTagName(item?.integration)} {item?.integration === 'direct_paging' ? ( <> Alert Groups raised manually via Web or ChatOps diff --git a/grafana-plugin/src/components/IntegrationInputField/IntegrationInputField.tsx b/grafana-plugin/src/components/IntegrationInputField/IntegrationInputField.tsx index 094598d3..566c7930 100644 --- a/grafana-plugin/src/components/IntegrationInputField/IntegrationInputField.tsx +++ b/grafana-plugin/src/components/IntegrationInputField/IntegrationInputField.tsx @@ -2,9 +2,8 @@ import React, { useState } from 'react'; import { HorizontalGroup, IconButton, Input } from '@grafana/ui'; import cn from 'classnames/bind'; -import CopyToClipboard from 'react-copy-to-clipboard'; -import { openNotification } from 'utils/utils'; +import CopyToClipboardIcon from 'components/CopyToClipboardIcon/CopyToClipboardIcon'; import styles from './IntegrationInputField.module.scss'; @@ -36,11 +35,7 @@ export const IntegrationInputField: React.FC = ({
{showEye && } - {showCopy && ( - - - - )} + {showCopy && } {showExternal && }
@@ -55,10 +50,6 @@ export const IntegrationInputField: React.FC = ({ setIsMasked(!isInputMasked); } - function onCopy() { - openNotification("Integration's HTTP Endpoint is copied"); - } - function onOpen() { window.open(value, '_blank'); } diff --git a/grafana-plugin/src/components/IntegrationLogo/IntegrationLogo.tsx b/grafana-plugin/src/components/IntegrationLogo/IntegrationLogo.tsx index ef69cfa8..5927653d 100644 --- a/grafana-plugin/src/components/IntegrationLogo/IntegrationLogo.tsx +++ b/grafana-plugin/src/components/IntegrationLogo/IntegrationLogo.tsx @@ -8,7 +8,7 @@ import { logoCoors } from './IntegrationLogo.config'; import styles from 'components/IntegrationLogo/IntegrationLogo.module.css'; -interface IntegrationLogoProps { +export interface IntegrationLogoProps { integration: SelectOption; scale: number; } diff --git a/grafana-plugin/src/components/IntegrationLogo/IntegrationLogoWithTitle.tsx b/grafana-plugin/src/components/IntegrationLogo/IntegrationLogoWithTitle.tsx new file mode 100644 index 00000000..54779833 --- /dev/null +++ b/grafana-plugin/src/components/IntegrationLogo/IntegrationLogoWithTitle.tsx @@ -0,0 +1,20 @@ +import React, { FC } from 'react'; + +import { HorizontalGroup } from '@grafana/ui'; + +import { Text } from 'components/Text/Text'; + +import { IntegrationLogo, IntegrationLogoProps } from './IntegrationLogo'; + +interface IntegrationLogoWithTitleProps { + integration: IntegrationLogoProps['integration']; +} + +const IntegrationLogoWithTitle: FC = ({ integration }) => ( + + + {integration?.display_name} + +); + +export default IntegrationLogoWithTitle; diff --git a/grafana-plugin/src/components/Integrations/IntegrationBlock.tsx b/grafana-plugin/src/components/Integrations/IntegrationBlock.tsx index 2fa6f44f..e640f2d2 100644 --- a/grafana-plugin/src/components/Integrations/IntegrationBlock.tsx +++ b/grafana-plugin/src/components/Integrations/IntegrationBlock.tsx @@ -11,9 +11,9 @@ const cx = cn.bind(styles); interface IntegrationBlockProps { className?: string; - noContent: boolean; + noContent?: boolean; heading: React.ReactNode; - content: React.ReactNode; + content?: React.ReactNode; toggle?: () => void; } diff --git a/grafana-plugin/src/components/Integrations/IntegrationTag.tsx b/grafana-plugin/src/components/Integrations/IntegrationTag.tsx new file mode 100644 index 00000000..15d8a19b --- /dev/null +++ b/grafana-plugin/src/components/Integrations/IntegrationTag.tsx @@ -0,0 +1,37 @@ +import React, { FC } from 'react'; + +import { css } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data'; +import { useStyles2 } from '@grafana/ui'; + +import { Tag } from 'components/Tag/Tag'; +import { Text } from 'components/Text/Text'; + +interface IntegrationTagProps { + children: React.ReactNode; +} + +export const IntegrationTag: FC = ({ children }) => { + const styles = useStyles2(getStyles); + + return ( + + + {children} + + + ); +}; + +export const getStyles = (theme: GrafanaTheme2) => ({ + tag: css({ + height: '25px', + background: theme.colors.background.secondary, + border: `1px solid ${theme.colors.border.weak}`, + }), + radius: css({ + borderRadius: '4px', + }), +}); + +export default IntegrationTag; diff --git a/grafana-plugin/src/components/MonacoEditor/MonacoEditor.tsx b/grafana-plugin/src/components/MonacoEditor/MonacoEditor.tsx index 8ded499c..1cb7e81f 100644 --- a/grafana-plugin/src/components/MonacoEditor/MonacoEditor.tsx +++ b/grafana-plugin/src/components/MonacoEditor/MonacoEditor.tsx @@ -1,6 +1,7 @@ -import React, { FC, useCallback } from 'react'; +import React, { ComponentProps, FC, useCallback } from 'react'; import { CodeEditor, CodeEditorSuggestionItemKind, LoadingPlaceholder } from '@grafana/ui'; +import cn from 'classnames'; import { getPaths } from 'utils/utils'; @@ -21,6 +22,8 @@ interface MonacoEditorProps { loading?: boolean; monacoOptions?: any; suggestionPrefix?: string; + containerClassName?: string; + codeEditorProps?: Partial>; } export enum MONACO_LANGUAGE { @@ -51,6 +54,8 @@ export const MonacoEditor: FC = (props) => { showLineNumbers = true, loading = false, suggestionPrefix = 'payload.', + containerClassName, + codeEditorProps, } = props; const autoCompleteList = useCallback( @@ -100,7 +105,8 @@ export const MonacoEditor: FC = (props) => { height={height} onEditorDidMount={handleMount} getSuggestions={useAutoCompleteList ? autoCompleteList : undefined} - containerStyles="u-width-height-100" + containerStyles={cn('u-width-height-100', containerClassName)} + {...codeEditorProps} /> ); }; diff --git a/grafana-plugin/src/components/SourceCode/SourceCode.tsx b/grafana-plugin/src/components/SourceCode/SourceCode.tsx index a1b798f8..deea3516 100644 --- a/grafana-plugin/src/components/SourceCode/SourceCode.tsx +++ b/grafana-plugin/src/components/SourceCode/SourceCode.tsx @@ -1,9 +1,10 @@ import React, { FC } from 'react'; -import { Button, IconButton, Tooltip } from '@grafana/ui'; +import { Button, IconButton } from '@grafana/ui'; import cn from 'classnames/bind'; import CopyToClipboard from 'react-copy-to-clipboard'; +import { formatSourceCodeJsonString } from 'utils/string'; import { openNotification } from 'utils/utils'; import styles from './SourceCode.module.scss'; @@ -14,33 +15,38 @@ interface SourceCodeProps { noMaxHeight?: boolean; showClipboardIconOnly?: boolean; showCopyToClipboard?: boolean; - children?: any; + children?: string; className?: string; + prettifyJsonString?: boolean; } -export const SourceCode: FC = (props) => { - const { children, noMaxHeight = false, showClipboardIconOnly = false, showCopyToClipboard = true, className } = props; +export const SourceCode: FC = ({ + children, + noMaxHeight = false, + showClipboardIconOnly = false, + showCopyToClipboard = true, + className, + prettifyJsonString, +}) => { const showClipboardCopy = showClipboardIconOnly || showCopyToClipboard; return (
{showClipboardCopy && ( { openNotification('Copied!'); }} > {showClipboardIconOnly ? ( - - - + ) : (
); diff --git a/grafana-plugin/src/components/Tabs/Tabs.tsx b/grafana-plugin/src/components/Tabs/Tabs.tsx index 6e6926d7..19e2e68e 100644 --- a/grafana-plugin/src/components/Tabs/Tabs.tsx +++ b/grafana-plugin/src/components/Tabs/Tabs.tsx @@ -1,9 +1,11 @@ -import React, { FC, useState } from 'react'; +import React, { FC, useEffect, useState } from 'react'; import { css } from '@emotion/css'; import { Tab, TabsBar, TabContent, useStyles2 } from '@grafana/ui'; import cn from 'classnames'; +import { LocationHelper } from 'utils/LocationHelper'; + interface TabConfig { label: string; content: React.ReactNode; @@ -11,24 +13,45 @@ interface TabConfig { interface TabsProps { tabs: TabConfig[]; - defaultActiveLabel?: string; tabContentClassName?: string; + shouldBeSyncedWithQueryString?: boolean; + // in case there are more than 1 in the page, we want to use different queryString keys + queryStringKey?: string; } -export const Tabs: FC = ({ tabs, defaultActiveLabel, tabContentClassName }) => { +export const Tabs: FC = ({ + tabs, + tabContentClassName, + shouldBeSyncedWithQueryString = true, + queryStringKey = 'activeTab', +}) => { const styles = useStyles2(getStyles); - const [activeTabLabel, setActiveTabLabel] = useState(defaultActiveLabel || tabs[0].label); + + const defaultActiveLabel = + (shouldBeSyncedWithQueryString && LocationHelper.getQueryParam(queryStringKey)) || tabs[0].label; + const [activeTabLabel, setActiveTabLabel] = useState(defaultActiveLabel); + + const setLabel = (label: string) => { + setActiveTabLabel(label); + if (shouldBeSyncedWithQueryString) { + LocationHelper.update({ [queryStringKey]: label }, 'partial'); + } + }; + + useEffect( + () => () => { + if (shouldBeSyncedWithQueryString) { + LocationHelper.update({ [queryStringKey]: undefined }, 'partial'); + } + }, + [] + ); return ( <> {tabs.map(({ label }) => ( - setActiveTabLabel(label)} - active={activeTabLabel === label} - /> + setLabel(label)} active={activeTabLabel === label} /> ))} diff --git a/grafana-plugin/src/components/Tag/Tag.module.css b/grafana-plugin/src/components/Tag/Tag.module.css index c8858482..905c3804 100644 --- a/grafana-plugin/src/components/Tag/Tag.module.css +++ b/grafana-plugin/src/components/Tag/Tag.module.css @@ -3,5 +3,11 @@ line-height: 100%; padding: 5px 8px; color: white; + display: inline-block; white-space: nowrap; } + +.size-small { + font-size: 12px; + height: 24px; +} diff --git a/grafana-plugin/src/components/Tag/Tag.tsx b/grafana-plugin/src/components/Tag/Tag.tsx index 48c7ab4a..c461dddb 100644 --- a/grafana-plugin/src/components/Tag/Tag.tsx +++ b/grafana-plugin/src/components/Tag/Tag.tsx @@ -8,27 +8,33 @@ interface TagProps { color?: string; className?: string; border?: string; + text?: string; children?: any; onClick?: (ev) => void; forwardedRef?: React.MutableRefObject; + size?: 'small' | 'medium'; } const cx = cn.bind(styles); export const Tag: FC = (props) => { - const { children, color, className, border, onClick } = props; + const { children, color, text, className, border, onClick, size = 'medium' } = props; const style: React.CSSProperties = {}; if (color) { style.backgroundColor = color; } + if (text) { + style.color = text; + } + if (border) { style.border = border; } return ( - + {children} ); diff --git a/grafana-plugin/src/components/Webhooks/WebhookLastEventDetails.tsx b/grafana-plugin/src/components/Webhooks/WebhookLastEventDetails.tsx new file mode 100644 index 00000000..f0261d12 --- /dev/null +++ b/grafana-plugin/src/components/Webhooks/WebhookLastEventDetails.tsx @@ -0,0 +1,129 @@ +import React, { FC, useMemo } from 'react'; + +import { css } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data'; +import { VerticalGroup, HorizontalGroup, Badge, useStyles2, Tooltip, Icon, useTheme2 } from '@grafana/ui'; +import dayjs from 'dayjs'; + +import { SourceCode } from 'components/SourceCode/SourceCode'; +import { Tabs } from 'components/Tabs/Tabs'; +import { Text } from 'components/Text/Text'; +import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types'; +import { getTzOffsetString } from 'models/timezone/timezone.helpers'; + +import WebhookStatusCodeBadge from './WebhookStatusCodeBadge'; + +interface WebhookLastEventDetailsProps { + webhook: OutgoingWebhook; +} + +const WebhookLastEventDetails: FC = ({ webhook }) => { + const styles = useStyles2(getStyles); + const theme = useTheme2(); + const rows = useMemo(() => getEventDetailsRows(theme, webhook), [theme, webhook]); + + if (!webhook.last_response_log?.timestamp) { + return ( + + An event triggering of this webhook has not been sent yet. + + ); + } + return ( + <> +
+ + {rows.map(({ title, value }) => ( + + {title} + {value} + + ))} + +
+ + {webhook.last_response_log.request_data || 'No data'} + + ), + }, + { + label: 'Response body', + content: ( + + {webhook.last_response_log.content || 'No data'} + + ), + }, + { + label: 'Request headers', + content: ( + + {webhook.last_response_log.request_headers || 'No data'} + + ), + }, + ]} + /> + + ); +}; + +const getEventDetailsRows = (theme: GrafanaTheme2, webhook?: OutgoingWebhook) => + webhook + ? [ + { + title: 'Trigger type', + value: webhook.trigger_type_name, + }, + { + title: 'Time', + value: `${dayjs(webhook.last_response_log?.timestamp).format('DD MMM YYYY, HH:mm')} (${getTzOffsetString( + dayjs(webhook.last_response_log?.timestamp) + )})`, + }, + { + title: 'URL', + value: ( + + {webhook.url} + {webhook.last_response_log?.url && webhook.url !== webhook.last_response_log?.url && ( + + + + )} + + ), + }, + { + title: 'Method', + value: , + }, + { + title: 'Response code', + value: , + }, + ] + : []; + +const getStyles = () => ({ + lastEventDetailsRowTitle: css({ + width: '150px', + }), + lastEventDetailsRowValue: css({ + fontWeight: 500, + }), + lastEventDetailsRowsWrapper: css({ + marginBottom: '26px', + }), + sourceCode: css({ + height: 'calc(100vh - 585px)', + }), +}); + +export default WebhookLastEventDetails; diff --git a/grafana-plugin/src/components/Webhooks/WebhookLastEventTimestamp.tsx b/grafana-plugin/src/components/Webhooks/WebhookLastEventTimestamp.tsx new file mode 100644 index 00000000..5b91632a --- /dev/null +++ b/grafana-plugin/src/components/Webhooks/WebhookLastEventTimestamp.tsx @@ -0,0 +1,72 @@ +import React from 'react'; + +import { css } from '@emotion/css'; +import { useTheme2, useStyles2, HorizontalGroup, Button } from '@grafana/ui'; +import dayjs from 'dayjs'; + +import { Tag } from 'components/Tag/Tag'; +import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types'; +import { getTzOffsetString } from 'models/timezone/timezone.helpers'; +import { OutgoingTabDrawerKey } from 'pages/integration/OutgoingTab/OutgoingTab.types'; + +import WebhookStatusCodeBadge from './WebhookStatusCodeBadge'; + +export const WebhookLastEventTimestamp = ({ + webhook, + openDrawer, +}: { + webhook: OutgoingWebhook; + openDrawer: (key: OutgoingTabDrawerKey) => void; +}) => { + const theme = useTheme2(); + const styles = useStyles2(getStyles); + + const lastEventMoment = dayjs(webhook.last_response_log?.timestamp); + + const lastEventFormatted = `${lastEventMoment.format('DD MMM YYYY')}, ${lastEventMoment.format( + 'HH:mm:ss' + )} (${getTzOffsetString(lastEventMoment)})`; + + const isLastEventDateValid = lastEventMoment.isValid(); + + if (!isLastEventDateValid) { + return ( + + Never + + ); + } + + return ( + + + {lastEventFormatted} + + + + {!is_webhook_enabled && } +
+ ); +}; + +export const getStyles = () => ({ + nameColumn: css({ + display: 'flex', + alignItems: 'center', + gap: '4px', + }), + webhookName: css({ + wordBreak: 'break-word', + padding: 0, + '&:hover': { + background: 'none', + }, + }), + disabledBadge: css({ + wordBreak: 'keep-all', + }), +}); diff --git a/grafana-plugin/src/components/Webhooks/WebhookStatusCodeBadge.tsx b/grafana-plugin/src/components/Webhooks/WebhookStatusCodeBadge.tsx new file mode 100644 index 00000000..aa967495 --- /dev/null +++ b/grafana-plugin/src/components/Webhooks/WebhookStatusCodeBadge.tsx @@ -0,0 +1,31 @@ +import React, { FC } from 'react'; + +import { css } from '@emotion/css'; +import { Badge, useStyles2 } from '@grafana/ui'; + +import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types'; + +interface WebhookStatusCodeBadgeProps { + webhook: OutgoingWebhook; +} + +const WebhookStatusCodeBadge: FC = ({ webhook }) => { + const styles = useStyles2(getStyles); + + return ( + + ); +}; + +const getStyles = () => ({ + lastEventBadge: css({ + wordBreak: 'keep-all', + whiteSpace: 'nowrap', + }), +}); + +export default WebhookStatusCodeBadge; diff --git a/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.tsx b/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.tsx index 2239c3b3..edbf91ed 100644 --- a/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.tsx +++ b/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.tsx @@ -16,7 +16,7 @@ import cn from 'classnames/bind'; import { observer } from 'mobx-react'; import CopyToClipboard from 'react-copy-to-clipboard'; -import { HamburgerMenu } from 'components/HamburgerMenu/HamburgerMenu'; +import { HamburgerMenuIcon } from 'components/HamburgerMenuIcon/HamburgerMenuIcon'; import { IntegrationCollapsibleTreeView, IntegrationCollapsibleItem, @@ -444,7 +444,7 @@ export const RouteButtonsDisplay: React.FC = ({ )} > {({ openMenu }) => ( - !isPresetFieldVisible(data.preset, presets, WebhookFormFieldName.TriggerType), normalize: (value) => value, @@ -126,32 +81,7 @@ export function createForm({ type: FormItemType.Select, extra: { placeholder: 'Choose (Required)', - options: [ - { - value: 'GET', - label: 'GET', - }, - { - value: 'POST', - label: 'POST', - }, - { - value: 'PUT', - label: 'PUT', - }, - { - value: 'PATCH', - label: 'PATCH', - }, - { - value: 'DELETE', - label: 'DELETE', - }, - { - value: 'OPTIONS', - label: 'OPTIONS', - }, - ], + options: HTTP_METHOD_OPTIONS, }, isHidden: (data) => !isPresetFieldVisible(data.preset, presets, WebhookFormFieldName.HttpMethod), normalize: (value) => value, diff --git a/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.module.css b/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.module.css index 5d2ed084..09b3e5b3 100644 --- a/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.module.css +++ b/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.module.css @@ -10,9 +10,8 @@ margin: 4px; } -.tabs__content { +.tabsWrapper { padding-top: 16px; - padding-bottom: 16px; } .form-row { diff --git a/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx b/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx index 92741afb..32f8f186 100644 --- a/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx +++ b/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx @@ -55,7 +55,7 @@ interface OutgoingWebhookFormProps { export const WebhookTabs = { Settings: new KeyValuePair('Settings', 'Settings'), - LastRun: new KeyValuePair('LastRun', 'Last Run'), + LastRun: new KeyValuePair('LastRun', 'Last Event'), }; const CustomFieldSectionRenderer: React.FC = observer(({ setValue, getValues }) => { @@ -275,30 +275,38 @@ export const OutgoingWebhookForm = observer((props: OutgoingWebhookFormProps) => return ( // show tabbed drawer (edit/live_run) <> - + + + { + setActiveTab(WebhookTabs.Settings.key); + history.push(`${PLUGIN_ROOT}/outgoing_webhooks/edit/${id}`); + }} + active={activeTab === WebhookTabs.Settings.key} + label={WebhookTabs.Settings.value} + /> + + { + setActiveTab(WebhookTabs.LastRun.key); + history.push(`${PLUGIN_ROOT}/outgoing_webhooks/last_run/${id}`); + }} + active={activeTab === WebhookTabs.LastRun.key} + label={WebhookTabs.LastRun.value} + /> + +
+ } + >
- - { - setActiveTab(WebhookTabs.Settings.key); - history.push(`${PLUGIN_ROOT}/outgoing_webhooks/edit/${id}`); - }} - active={activeTab === WebhookTabs.Settings.key} - label={WebhookTabs.Settings.value} - /> - - { - setActiveTab(WebhookTabs.LastRun.key); - history.push(`${PLUGIN_ROOT}/outgoing_webhooks/last_run/${id}`); - }} - active={activeTab === WebhookTabs.LastRun.key} - label={WebhookTabs.LastRun.value} - /> - - )} @@ -392,7 +400,7 @@ interface WebhookTabsProps { } const WebhookTabsContent: React.FC = observer( - ({ id, action, activeTab, data, onHide, onUpdate, onDelete, formElement }) => { + ({ id, action, activeTab, data, onHide, onDelete, formElement }) => { const [confirmationModal, setConfirmationModal] = useState(undefined); const { outgoingWebhookStore, hasFeature, grafanaTeamStore, alertReceiveChannelStore } = useStore(); const form = createForm({ @@ -441,7 +449,7 @@ const WebhookTabsContent: React.FC = observer( @@ -456,7 +464,7 @@ const WebhookTabsContent: React.FC = observer( )} )} - {activeTab === WebhookTabs.LastRun.key && } + {activeTab === WebhookTabs.LastRun.key && }
); } diff --git a/grafana-plugin/src/containers/OutgoingWebhookStatus/OutgoingWebhookStatus.tsx b/grafana-plugin/src/containers/OutgoingWebhookStatus/OutgoingWebhookStatus.tsx index 7e726a2f..1cb47e31 100644 --- a/grafana-plugin/src/containers/OutgoingWebhookStatus/OutgoingWebhookStatus.tsx +++ b/grafana-plugin/src/containers/OutgoingWebhookStatus/OutgoingWebhookStatus.tsx @@ -1,14 +1,13 @@ import React from 'react'; -import { Label, VerticalGroup } from '@grafana/ui'; +import { HorizontalGroup, Button } from '@grafana/ui'; import cn from 'classnames/bind'; import { observer } from 'mobx-react'; -import { Block } from 'components/GBlock/Block'; -import { SourceCode } from 'components/SourceCode/SourceCode'; -import { Text } from 'components/Text/Text'; +import WebhookLastEventDetails from 'components/Webhooks/WebhookLastEventDetails'; import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types'; import { useStore } from 'state/useStore'; +import { useCommonStyles } from 'utils/hooks'; import styles from 'containers/OutgoingWebhookForm/OutgoingWebhookForm.module.css'; @@ -16,105 +15,27 @@ const cx = cn.bind(styles); interface OutgoingWebhookStatusProps { id: OutgoingWebhook['id']; - onUpdate: () => void; + closeDrawer: () => void; } -function Debug(props) { - return ( - - - - - {props.source && {props.source}} - {props.result && props.result !== props.source && ( - - - {props.result} - - )} - - - - ); -} - -function format_response_field(str) { - try { - const jsonValue = JSON.parse(str); - return JSON.stringify(jsonValue, null, 4); - } catch (e) { - return str; - } -} - -export const OutgoingWebhookStatus = observer((props: OutgoingWebhookStatusProps) => { - const { id } = props; - - const store = useStore(); - - const { outgoingWebhookStore } = store; - - const data = outgoingWebhookStore.items[id]; +export const OutgoingWebhookStatus = observer(({ id, closeDrawer }: OutgoingWebhookStatusProps) => { + const { + outgoingWebhookStore: { + items: { [id]: webhook }, + }, + } = useStore(); + const commonStyles = useCommonStyles(); return (
- - - {data.name} - - {data.id} - - {data.trigger_type_name} - - {data.last_response_log.timestamp ? ( - - - {data.last_response_log.timestamp} - - {data.last_response_log.url && ( - - )} - {data.last_response_log.status_code && ( - - - {data.last_response_log.status_code} - - )} - - {data.last_response_log.content && ( - - - {format_response_field(data.last_response_log.content)} - - )} - {data.last_response_log.request_trigger && ( - - )} - {data.last_response_log.request_headers && ( - - )} - {data.last_response_log.request_data && ( - - )} - - ) : ( - - An event triggering this webhook has not been sent yet! - - )} - + +
+ + + +
); }); diff --git a/grafana-plugin/src/containers/WebhooksTemplateEditor/WebhooksTemplateEditor.tsx b/grafana-plugin/src/containers/WebhooksTemplateEditor/WebhooksTemplateEditor.tsx index cb67779f..f93a0bf1 100644 --- a/grafana-plugin/src/containers/WebhooksTemplateEditor/WebhooksTemplateEditor.tsx +++ b/grafana-plugin/src/containers/WebhooksTemplateEditor/WebhooksTemplateEditor.tsx @@ -20,8 +20,8 @@ const cx = cn.bind(styles); interface Template { value: string; displayName: string; - description: string; - name: undefined; + description?: string; + name: string; } interface WebhooksTemplateEditorProps { @@ -37,9 +37,9 @@ export const WebhooksTemplateEditor: React.FC = ({ onHide, handleSubmit, }) => { - const [isCheatSheetVisible, setIsCheatSheetVisible] = useState(false); - const [changedTemplateBody, setChangedTemplateBody] = useState(template.value); - const [selectedPayload, setSelectedPayload] = useState(undefined); + const [isCheatSheetVisible, setIsCheatSheetVisible] = useState(false); + const [changedTemplateBody, setChangedTemplateBody] = useState(template.value); + const [selectedPayload, setSelectedPayload] = useState(); const [resultError, setResultError] = useState(undefined); const getChangeHandler = () => { diff --git a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts index cc431298..999ba481 100644 --- a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts +++ b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts @@ -94,11 +94,11 @@ export class AlertReceiveChannelStore { } async fetchItems(query: any = '') { - const params = typeof query === 'string' ? { search: query } : query; - const { data: { results }, - } = await onCallApi().GET('/alert_receive_channels/', { params }); + } = await onCallApi().GET('/alert_receive_channels/', { + params: { query: typeof query === 'string' ? { search: query } : query }, + }); runInAction(() => { this.items = { diff --git a/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.types.ts b/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.types.ts index db773cf3..59faf1d6 100644 --- a/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.types.ts +++ b/grafana-plugin/src/models/outgoing_webhook/outgoing_webhook.types.ts @@ -1,5 +1,6 @@ import { GrafanaTeam } from 'models/grafana_team/grafana_team.types'; import { LabelKeyValue } from 'models/label/label.types'; +import { KeyValuePair } from 'utils/utils'; export interface OutgoingWebhook { authorization_header: string; @@ -41,3 +42,81 @@ export interface OutgoingWebhookPreset { logo: string; controlled_fields: string[]; } + +export const WebhookTriggerType = { + EscalationStep: new KeyValuePair('0', 'Escalation Step'), + AlertGroupCreated: new KeyValuePair('1', 'Alert Group Created'), + Acknowledged: new KeyValuePair('2', 'Acknowledged'), + Resolved: new KeyValuePair('3', 'Resolved'), + Silenced: new KeyValuePair('4', 'Silenced'), + Unsilenced: new KeyValuePair('5', 'Unsilenced'), + Unresolved: new KeyValuePair('6', 'Unresolved'), + Unacknowledged: new KeyValuePair('7', 'Unacknowledged'), + AlertGroupStatusChange: new KeyValuePair('8', 'Alert Group Status Change'), +}; + +export const WEBHOOK_TRIGGGER_TYPE_OPTIONS = [ + { + value: WebhookTriggerType.EscalationStep.key, + label: WebhookTriggerType.EscalationStep.value, + }, + { + value: WebhookTriggerType.AlertGroupCreated.key, + label: WebhookTriggerType.AlertGroupCreated.value, + }, + { + value: WebhookTriggerType.AlertGroupStatusChange.key, + label: WebhookTriggerType.AlertGroupStatusChange.value, + }, + { + value: WebhookTriggerType.Acknowledged.key, + label: WebhookTriggerType.Acknowledged.value, + }, + { + value: WebhookTriggerType.Resolved.key, + label: WebhookTriggerType.Resolved.value, + }, + { + value: WebhookTriggerType.Silenced.key, + label: WebhookTriggerType.Silenced.value, + }, + { + value: WebhookTriggerType.Unsilenced.key, + label: WebhookTriggerType.Unsilenced.value, + }, + { + value: WebhookTriggerType.Unresolved.key, + label: WebhookTriggerType.Unresolved.value, + }, + { + value: WebhookTriggerType.Unacknowledged.key, + label: WebhookTriggerType.Unacknowledged.value, + }, +]; + +export const HTTP_METHOD_OPTIONS = [ + { + value: 'GET', + label: 'GET', + }, + { + value: 'POST', + label: 'POST', + }, + { + value: 'PUT', + label: 'PUT', + }, + { + value: 'PATCH', + label: 'PATCH', + }, + { + value: 'DELETE', + label: 'DELETE', + }, + { + value: 'OPTIONS', + label: 'OPTIONS', + }, +]; diff --git a/grafana-plugin/src/pages/integration/Integration.module.scss b/grafana-plugin/src/pages/integration/Integration.module.scss index 6b71d115..96650a4b 100644 --- a/grafana-plugin/src/pages/integration/Integration.module.scss +++ b/grafana-plugin/src/pages/integration/Integration.module.scss @@ -126,9 +126,6 @@ $LARGE-MARGIN: 24px; align-items: center; gap: 8px; } -.tag { - height: 25px; -} .heartbeat-badge { padding: 4px 8px; @@ -208,10 +205,6 @@ $LARGE-MARGIN: 24px; } } -.radius { - border-radius: 4px; -} - .inline-switch { height: 34px; border: var(--border-weak); diff --git a/grafana-plugin/src/pages/integration/Integration.tsx b/grafana-plugin/src/pages/integration/Integration.tsx index 363ef31d..1e6c42aa 100644 --- a/grafana-plugin/src/pages/integration/Integration.tsx +++ b/grafana-plugin/src/pages/integration/Integration.tsx @@ -22,25 +22,23 @@ import { RouteComponentProps, useHistory, withRouter } from 'react-router-dom'; import { getTemplatesForEdit } from 'components/AlertTemplates/AlertTemplatesForm.config'; import { TemplateForEdit } from 'components/AlertTemplates/CommonAlertTemplatesForm.config'; -import { HamburgerMenu } from 'components/HamburgerMenu/HamburgerMenu'; +import { HamburgerContextMenu } from 'components/HamburgerContextMenu/HamburgerContextMenu'; import { IntegrationCollapsibleTreeView, IntegrationCollapsibleItem, } from 'components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView'; import { IntegrationContactPoint } from 'components/IntegrationContactPoint/IntegrationContactPoint'; import { IntegrationHowToConnect } from 'components/IntegrationHowToConnect/IntegrationHowToConnect'; -import { IntegrationLogo } from 'components/IntegrationLogo/IntegrationLogo'; +import IntegrationLogoWithTitle from 'components/IntegrationLogo/IntegrationLogoWithTitle'; import { IntegrationSendDemoAlertModal } from 'components/IntegrationSendDemoAlertModal/IntegrationSendDemoAlertModal'; import { IntegrationBlock } from 'components/Integrations/IntegrationBlock'; +import { IntegrationTag } from 'components/Integrations/IntegrationTag'; import { PageErrorHandlingWrapper, PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper'; import { initErrorDataState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers'; import { PluginLink } from 'components/PluginLink/PluginLink'; -import { RenderConditionally } from 'components/RenderConditionally/RenderConditionally'; import { Tabs } from 'components/Tabs/Tabs'; -import { Tag } from 'components/Tag/Tag'; import { Text } from 'components/Text/Text'; import { TooltipBadge } from 'components/TooltipBadge/TooltipBadge'; -import { WithContextMenu } from 'components/WithContextMenu/WithContextMenu'; import { EditRegexpRouteTemplateModal } from 'containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal'; import { CollapsedIntegrationRouteDisplay } from 'containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay'; import { ExpandedIntegrationRouteDisplay } from 'containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay'; @@ -65,7 +63,6 @@ import { AppFeature } from 'state/features'; import { PageProps, SelectOption, WithStoreProps } from 'state/types'; import { useStore } from 'state/useStore'; import { withMobXProviderContext } from 'state/withStore'; -import { getVar } from 'utils/DOM'; import { LocationHelper } from 'utils/LocationHelper'; import { UserActions } from 'utils/authorization/authorization'; import { PLUGIN_ROOT } from 'utils/consts'; @@ -73,6 +70,8 @@ import { getItem, setItem } from 'utils/localStorage'; import { sanitize } from 'utils/sanitize'; import { openNotification, openErrorNotification } from 'utils/utils'; +import { OutgoingTab } from './OutgoingTab/OutgoingTab'; + const cx = cn.bind(styles); interface IntegrationProps extends WithStoreProps, PageProps, RouteComponentProps<{ id: string }> {} @@ -261,7 +260,7 @@ class _IntegrationPage extends React.Componentoutgoing tab content }, + { label: 'Outgoing', content: }, ]} /> ) : ( @@ -467,15 +466,7 @@ class _IntegrationPage extends React.Component - - - Templates - - + Templates
@@ -925,165 +916,132 @@ const IntegrationActions: React.FC = ({
- ( -
-
openIntegrationSettings()}> - Integration Settings -
- - {store.hasFeature(AppFeature.Labels) && ( - -
openLabelsForm()}> - Alert group labeling -
-
- )} - - {showHeartbeatSettings() && ( - -
setIsHeartbeatFormOpen(true)} - data-testid="integration-heartbeat-settings" - > - Heartbeat Settings -
-
- )} - - {!alertReceiveChannel.maintenance_till && ( - -
- Start Maintenance -
-
- )} - - -
- Edit Templates -
-
- - {alertReceiveChannel.maintenance_till && ( - -
{ - setConfirmModal({ - isOpen: true, - confirmText: 'Stop', - dismissText: 'Cancel', - onConfirm: onStopMaintenance, - title: 'Stop Maintenance', - body: ( - - Are you sure you want to stop the maintenance for{' '} - ? - - ), - }); - }} - data-testid="integration-stop-maintenance" - > - Stop Maintenance -
-
- )} - - {isLegacyIntegration && ( - -
- setConfirmModal({ - isOpen: true, - title: 'Migrate Integration?', - body: ( - - - Are you sure you want to migrate ? - - - - - Integration internal behaviour will be changed - - - Integration URL will stay the same, so no need to change {getMigrationDisplayName()}{' '} - configuration - - - - Integration templates will be reset to suit the new payload - - - - It is needed to adjust routes manually to the new payload - - - - ), - onConfirm: onIntegrationMigrate, - dismissText: 'Cancel', - confirmText: 'Migrate', - }) - } - > - Migrate -
-
- )} - - openNotification('Integration ID is copied')} - > -
- - - - UID: {alertReceiveChannel.id} - -
-
- -
- -
-
{ - setConfirmModal({ - isOpen: true, - title: 'Delete Integration?', - body: ( - - Are you sure you want to delete ? - - ), - onConfirm: deleteIntegration, - dismissText: 'Cancel', - confirmText: 'Delete', - }); - }} - className="u-width-100" - > - - - - Delete Integration - + setIsHeartbeatFormOpen(true), + hidden: !showHeartbeatSettings(), + label:
Heartbeat Settings
, + requiredPermission: UserActions.IntegrationsWrite, + }, + { + onClick: openStartMaintenance, + hidden: Boolean(alertReceiveChannel.maintenance_till), + label: 'Start Maintenance', + requiredPermission: UserActions.MaintenanceWrite, + }, + { + onClick: changeIsTemplateSettingsOpen, + label: 'Edit Templates', + requiredPermission: UserActions.MaintenanceWrite, + }, + { + onClick: () => { + setConfirmModal({ + isOpen: true, + confirmText: 'Stop', + dismissText: 'Cancel', + onConfirm: onStopMaintenance, + title: 'Stop Maintenance', + body: ( + + Are you sure you want to stop the maintenance for{' '} + ? + + ), + }); + }, + hidden: !alertReceiveChannel.maintenance_till, + label: 'Stop Maintenance', + requiredPermission: UserActions.MaintenanceWrite, + }, + { + onClick: () => + setConfirmModal({ + isOpen: true, + title: 'Migrate Integration?', + body: ( + + + Are you sure you want to migrate ? -
+ + + - Integration internal behaviour will be changed + + - Integration URL will stay the same, so no need to change {getMigrationDisplayName()}{' '} + configuration + + - Integration templates will be reset to suit the new payload + - It is needed to adjust routes manually to the new payload + + + ), + onConfirm: onIntegrationMigrate, + dismissText: 'Cancel', + confirmText: 'Migrate', + }), + hidden: !isLegacyIntegration, + label: 'Migrate', + requiredPermission: UserActions.IntegrationsWrite, + }, + { + label: ( + openNotification('Integration ID is copied')} + > +
+ + + UID: {alertReceiveChannel.id} +
- - -
- )} - > - {({ openMenu }) => } - + + ), + }, + { + onClick: () => { + setConfirmModal({ + isOpen: true, + title: 'Delete Integration?', + body: ( + + Are you sure you want to delete ? + + ), + onConfirm: deleteIntegration, + dismissText: 'Cancel', + confirmText: 'Delete', + }); + }, + hidden: !alertReceiveChannel.allow_delete, + label: ( + + + + Delete Integration + + + ), + requiredPermission: UserActions.IntegrationsWrite, + }, + ]} + />
@@ -1231,10 +1189,7 @@ const IntegrationHeader: React.FC = ({
Type: - - - {integration?.display_name} - +
Team: diff --git a/grafana-plugin/src/pages/integration/OutgoingTab/ConnectIntegrationModal.tsx b/grafana-plugin/src/pages/integration/OutgoingTab/ConnectIntegrationModal.tsx new file mode 100644 index 00000000..8f0677eb --- /dev/null +++ b/grafana-plugin/src/pages/integration/OutgoingTab/ConnectIntegrationModal.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +import { Icon, Input, Modal, useStyles2 } from '@grafana/ui'; + +import { Text } from 'components/Text/Text'; + +import ConnectedIntegrationsTable from './ConnectedIntegrationsTable'; +import { getStyles } from './OutgoingTab.styles'; + +export const ConnectIntegrationModal = ({ onDismiss }: { onDismiss: () => void }) => { + const styles = useStyles2(getStyles); + + return ( + Connect integration} + closeOnBackdropClick={false} + closeOnEscape + onDismiss={onDismiss} + > + } + placeholder="Search integrations..." + /> + + + ); +}; diff --git a/grafana-plugin/src/pages/integration/OutgoingTab/ConnectedIntegrationsTable.tsx b/grafana-plugin/src/pages/integration/OutgoingTab/ConnectedIntegrationsTable.tsx new file mode 100644 index 00000000..d95bea53 --- /dev/null +++ b/grafana-plugin/src/pages/integration/OutgoingTab/ConnectedIntegrationsTable.tsx @@ -0,0 +1,73 @@ +import React, { FC } from 'react'; + +import { HorizontalGroup, Tooltip, Icon, useStyles2, IconButton, Switch } from '@grafana/ui'; + +import { GTable } from 'components/GTable/GTable'; +import IntegrationLogoWithTitle from 'components/IntegrationLogo/IntegrationLogoWithTitle'; +import { Text } from 'components/Text/Text'; + +import { getStyles } from './OutgoingTab.styles'; + +interface ConnectedIntegrationsTableProps { + allowDelete?: boolean; +} + +const ConnectedIntegrationsTable: FC = (props) => { + const FAKE_INTEGRATIONS = [{ a: 'a' }]; + + return ( + + ); +}; + +const getColumns = ({ allowDelete }: ConnectedIntegrationsTableProps) => [ + { + width: '45%', + title: Integration name, + dataIndex: 'trigger_type_name', + render: () => <>Some integration name, + }, + { + width: '55%', + title: Type, + render: () => , + }, + { + title: ( + + Backsync + Switch on to start sending data from other integrations}> + + + + ), + render: BacksyncSwitcher, + }, + { + render: () => , + }, +]; + +const BacksyncSwitcher = () => { + const styles = useStyles2(getStyles); + + return ( +
+ +
+ ); +}; + +const ActionsColumn = ({ allowDelete }: { allowDelete?: boolean }) => ( + + + {allowDelete && } + +); + +export default ConnectedIntegrationsTable; diff --git a/grafana-plugin/src/pages/integration/OutgoingTab/NewOutgoingWebhookDrawerContent.tsx b/grafana-plugin/src/pages/integration/OutgoingTab/NewOutgoingWebhookDrawerContent.tsx new file mode 100644 index 00000000..8c5b7938 --- /dev/null +++ b/grafana-plugin/src/pages/integration/OutgoingTab/NewOutgoingWebhookDrawerContent.tsx @@ -0,0 +1,46 @@ +import React, { FC } from 'react'; + +import { Button, HorizontalGroup, useStyles2, VerticalGroup } from '@grafana/ui'; +import { useForm, FormProvider } from 'react-hook-form'; + +import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; +import { UserActions } from 'utils/authorization/authorization'; +import { useCommonStyles } from 'utils/hooks'; + +import { getStyles } from './OutgoingTab.styles'; +import { OutgoingTabFormValues } from './OutgoingTab.types'; +import { OutgoingWebhookFormFields } from './OutgoingWebhookFormFields'; + +interface NewOutgoingWebhookDrawerContentProps { + closeDrawer: () => void; +} + +export const NewOutgoingWebhookDrawerContent: FC = ({ closeDrawer }) => { + const styles = useStyles2(getStyles); + const commonStyles = useCommonStyles(); + const formMethods = useForm({ mode: 'all' }); + + const onSubmit = () => {}; + + return ( + +
+ +
+ +
+
+ + + + + + +
+
+
+
+ ); +}; diff --git a/grafana-plugin/src/pages/integration/OutgoingTab/OtherIntegrations.tsx b/grafana-plugin/src/pages/integration/OutgoingTab/OtherIntegrations.tsx new file mode 100644 index 00000000..dfb5a813 --- /dev/null +++ b/grafana-plugin/src/pages/integration/OutgoingTab/OtherIntegrations.tsx @@ -0,0 +1,31 @@ +import React, { useState } from 'react'; + +import { Button, HorizontalGroup } from '@grafana/ui'; +import { observer } from 'mobx-react-lite'; + +import { IntegrationBlock } from 'components/Integrations/IntegrationBlock'; +import IntegrationTag from 'components/Integrations/IntegrationTag'; + +import { ConnectIntegrationModal } from './ConnectIntegrationModal'; +import ConnectedIntegrationsTable from './ConnectedIntegrationsTable'; + +export const OtherIntegrations = observer(() => { + const [isConnectModalOpened, setIsConnectModalOpened] = useState(false); + + return ( + <> + {isConnectModalOpened && setIsConnectModalOpened(false)} />} + + Send data from other integrations + + + } + content={} + /> + + ); +}); diff --git a/grafana-plugin/src/pages/integration/OutgoingTab/OutgoingTab.styles.ts b/grafana-plugin/src/pages/integration/OutgoingTab/OutgoingTab.styles.ts new file mode 100644 index 00000000..fd0e63a2 --- /dev/null +++ b/grafana-plugin/src/pages/integration/OutgoingTab/OutgoingTab.styles.ts @@ -0,0 +1,77 @@ +import { css } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data'; + +export const getStyles = (theme: GrafanaTheme2) => ({ + urlIntegrationBlock: css({ + marginBottom: '32px', + }), + urlInput: css({ + height: '25px', + background: theme.colors.background.canvas, + '& input': { + height: '25px', + }, + }), + form: css({ + height: '100%', + paddingBottom: '64px', + marginTop: '32px', + }), + formFieldsWrapper: css({ + width: '100%', + }), + infoIcon: css({ + marginLeft: '4px', + }), + monacoEditorLabelWrapper: css({ + display: 'flex', + width: '100%', + }), + switcherFieldWrapper: css({ + display: 'flex', + gap: '8px', + alignItems: 'center', + }), + switcherLabel: css({ + marginBottom: 0, + }), + selectField: css({ + width: '200px', + marginBottom: 0, + }), + hamburgerIcon: css({ + background: theme.colors.secondary.shade, + }), + horizontalGroup: css({ + display: 'flex', + gap: '8px', + }), + openConfigurationBtn: css({ + background: theme.colors.secondary.shade, + }), + outgoingWebhooksTable: css({ + margin: '24px 0', + }), + backsyncColumn: css({ + display: 'flex', + justifyContent: 'flex-end', + }), + triggerTemplateWrapper: css({ + position: 'relative', + width: '100%', + }), + addTriggerTemplate: css({ + marginBottom: '16px', + }), + editTriggerTemplateBtn: css({ + position: 'absolute', + top: '-8px', + right: 0, + }), + searchIntegrationsInput: css({ + marginBottom: '24px', + }), + tabsWrapper: css({ + padding: '16px 16px 16px 8px', + }), +}); diff --git a/grafana-plugin/src/pages/integration/OutgoingTab/OutgoingTab.tsx b/grafana-plugin/src/pages/integration/OutgoingTab/OutgoingTab.tsx new file mode 100644 index 00000000..6eb93be1 --- /dev/null +++ b/grafana-plugin/src/pages/integration/OutgoingTab/OutgoingTab.tsx @@ -0,0 +1,114 @@ +import React from 'react'; + +import { useStyles2, Input, IconButton, Button, Drawer, Badge } from '@grafana/ui'; + +import CopyToClipboardIcon from 'components/CopyToClipboardIcon/CopyToClipboardIcon'; +import { IntegrationCollapsibleTreeView } from 'components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView'; +import { IntegrationBlock } from 'components/Integrations/IntegrationBlock'; +import { IntegrationTag } from 'components/Integrations/IntegrationTag'; +import { useDrawer } from 'utils/hooks'; + +import { NewOutgoingWebhookDrawerContent } from './NewOutgoingWebhookDrawerContent'; +import { OtherIntegrations } from './OtherIntegrations'; +import { getStyles } from './OutgoingTab.styles'; +import { OutgoingTabDrawerKey } from './OutgoingTab.types'; +import { OutgoingWebhookDetailsDrawerTabs } from './OutgoingWebhookDetailsDrawerTabs'; +import { OutgoingWebhooksTable } from './OutgoingWebhooksTable'; + +export const OutgoingTab = () => { + const { openDrawer, closeDrawer, getIsDrawerOpened } = useDrawer(); + + return ( + <> + {getIsDrawerOpened('webhookDetails') && ( + } + > +
+ + )} + {getIsDrawerOpened('newOutgoingWebhook') && ( + + + + )} + , + }, + { + customIcon: 'plus', + expandedView: () => ( + <> + + + + ), + }, + { + customIcon: 'exchange-alt', + startingElemPosition: '50%', + expandedView: () => , + }, + ]} + /> + + ); +}; + +const Connection = () => { + const styles = useStyles2(getStyles); + const FAKE_URL = 'https://example.com'; + + const value = FAKE_URL; + + return ( +
+ + ServiceNow connection + + + + window.open(value, '_blank')} + /> + + } + /> +
+ } + /> +

Outgoing webhooks

+
+ ); +}; + +const AddOutgoingWebhook = ({ openDrawer }: { openDrawer: (key: OutgoingTabDrawerKey) => void }) => ( + +); diff --git a/grafana-plugin/src/pages/integration/OutgoingTab/OutgoingTab.types.ts b/grafana-plugin/src/pages/integration/OutgoingTab/OutgoingTab.types.ts new file mode 100644 index 00000000..fe195ebc --- /dev/null +++ b/grafana-plugin/src/pages/integration/OutgoingTab/OutgoingTab.types.ts @@ -0,0 +1,23 @@ +export type OutgoingTabDrawerKey = 'webhookDetails' | 'newOutgoingWebhook'; + +export const TriggerDetailsQueryStringKey = { + ActiveTab: 'activeEventTriggerDrawerTab', + WebhookId: 'webhookId', +}; + +export const TriggerDetailsTab = { + Settings: 'Settings', + LastEvent: 'Last event', +} as const; +export type TriggerDetailsTab = (typeof TriggerDetailsTab)[keyof typeof TriggerDetailsTab]; + +export interface OutgoingTabFormValues { + triggerType: string; + isEnabled?: boolean; + url: string; + httpMethod: string; + triggerTemplateToogle?: boolean; + triggerTemplate?: string; + forwardedDataTemplateToogle?: boolean; + forwardedDataTemplate?: string; +} diff --git a/grafana-plugin/src/pages/integration/OutgoingTab/OutgoingWebhookDetailsDrawerTabs.tsx b/grafana-plugin/src/pages/integration/OutgoingTab/OutgoingWebhookDetailsDrawerTabs.tsx new file mode 100644 index 00000000..a2e84133 --- /dev/null +++ b/grafana-plugin/src/pages/integration/OutgoingTab/OutgoingWebhookDetailsDrawerTabs.tsx @@ -0,0 +1,111 @@ +import React, { FC } from 'react'; + +import { Button, HorizontalGroup, useStyles2, VerticalGroup } from '@grafana/ui'; +import { observer } from 'mobx-react-lite'; +import { useForm, FormProvider } from 'react-hook-form'; + +import { Tabs } from 'components/Tabs/Tabs'; +import WebhookLastEventDetails from 'components/Webhooks/WebhookLastEventDetails'; +import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; +import { useStore } from 'state/useStore'; +import { LocationHelper } from 'utils/LocationHelper'; +import { UserActions } from 'utils/authorization/authorization'; +import { useCommonStyles } from 'utils/hooks'; + +import { getStyles } from './OutgoingTab.styles'; +import { OutgoingTabFormValues, TriggerDetailsQueryStringKey, TriggerDetailsTab } from './OutgoingTab.types'; +import { OutgoingWebhookFormFields } from './OutgoingWebhookFormFields'; + +interface OutgoingWebhookDetailsDrawerTabsProps { + closeDrawer: () => void; +} +export const OutgoingWebhookDetailsDrawerTabs: FC = ({ closeDrawer }) => { + const styles = useStyles2(getStyles); + + return ( +
+ }, + { label: TriggerDetailsTab.LastEvent, content: }, + ]} + /> +
+ ); +}; + +interface SettingsProps { + closeDrawer: () => void; +} +const Settings: FC = ({ closeDrawer }) => { + const styles = useStyles2(getStyles); + const commonStyles = useCommonStyles(); + const form = useForm({ mode: 'all' }); + const webhookId = LocationHelper.getQueryParam(TriggerDetailsQueryStringKey.WebhookId); + + const onSubmit = () => {}; + + return ( + +
+ +
+ +
+
+ + + + + + + + + +
+
+
+
+ ); +}; + +interface LastEventDetailsProps { + closeDrawer: () => void; +} +const LastEventDetails: FC = observer(({ closeDrawer }) => { + const commonStyles = useCommonStyles(); + + const { + outgoingWebhookStore: { items }, + } = useStore(); + const webhook = items[LocationHelper.getQueryParam(TriggerDetailsQueryStringKey.WebhookId)]; + + if (!webhook) { + return null; + } + + return ( +
+ +
+ + + +
+
+ ); +}); diff --git a/grafana-plugin/src/pages/integration/OutgoingTab/OutgoingWebhookFormFields.tsx b/grafana-plugin/src/pages/integration/OutgoingTab/OutgoingWebhookFormFields.tsx new file mode 100644 index 00000000..b3ef5ffc --- /dev/null +++ b/grafana-plugin/src/pages/integration/OutgoingTab/OutgoingWebhookFormFields.tsx @@ -0,0 +1,230 @@ +import React, { FC, useState } from 'react'; + +import { + Button, + Field, + HorizontalGroup, + Icon, + Input, + Label, + Select, + Switch, + Tooltip, + useStyles2, + VerticalGroup, +} from '@grafana/ui'; +import cn from 'classnames'; +import { Controller, useFormContext } from 'react-hook-form'; + +import { MonacoEditor } from 'components/MonacoEditor/MonacoEditor'; +import { MONACO_READONLY_CONFIG } from 'components/MonacoEditor/MonacoEditor.config'; +import { WebhooksTemplateEditor } from 'containers/WebhooksTemplateEditor/WebhooksTemplateEditor'; +import { HTTP_METHOD_OPTIONS, WEBHOOK_TRIGGGER_TYPE_OPTIONS } from 'models/outgoing_webhook/outgoing_webhook.types'; + +import { getStyles } from './OutgoingTab.styles'; +import { OutgoingTabFormValues } from './OutgoingTab.types'; + +interface TemplateToEdit { + value: string; + displayName: string; + name: string; +} + +interface OutgoingWebhookFormFieldsProps { + // "new" should be used for new webhook + webhookId: string; +} + +export const OutgoingWebhookFormFields: FC = ({ webhookId }) => { + const styles = useStyles2(getStyles); + const { control, watch, formState, register } = useFormContext(); + const [templateToEdit, setTemplateToEdit] = useState(); + + const [showTriggerTemplate] = watch(['triggerTemplateToogle', 'forwardedDataTemplateToogle']); + + return ( + +
+ onChange(!value)} />} + /> + +
+ ( + + Trigger type  + + + + + } + className={styles.selectField} + > + + + ( + + HTTP method  + + + + + } + className={styles.selectField} + > +