diff --git a/grafana-plugin/package.json b/grafana-plugin/package.json index e888a52c..7cd31356 100644 --- a/grafana-plugin/package.json +++ b/grafana-plugin/package.json @@ -153,6 +153,8 @@ "dayjs": "^1.11.5", "eslint-plugin-import": "^2.29.1", "immutability-helper": "^3.1.1", + "linkify-react": "^4.1.3", + "linkifyjs": "^4.1.3", "mobx": "6.12.0", "mobx-react": "9.1.0", "object-hash": "^3.0.0", diff --git a/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.tsx b/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.tsx index 709281fc..46dbac0b 100644 --- a/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.tsx +++ b/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.tsx @@ -18,7 +18,6 @@ import { useStyles2, } from '@grafana/ui'; import { observer } from 'mobx-react'; -import { parseUrl } from 'query-string'; import { Controller, useForm, useFormContext, FormProvider } from 'react-hook-form'; import { useHistory } from 'react-router-dom'; @@ -40,6 +39,7 @@ import { useStore } from 'state/useStore'; import { UserActions } from 'utils/authorization/authorization'; import { PLUGIN_ROOT, generateAssignToTeamInputDescription, DOCS_ROOT, INTEGRATION_SERVICENOW } from 'utils/consts'; import { useIsLoading } from 'utils/hooks'; +import { validateURL } from 'utils/string'; import { OmitReadonlyMembers } from 'utils/types'; import { prepareForEdit } from './IntegrationForm.helpers'; @@ -406,10 +406,6 @@ export const IntegrationForm = observer( ); } - function validateURL(urlFieldValue: string): string | boolean { - return !parseUrl(urlFieldValue) ? 'Instance URL is invalid' : true; - } - async function onFormSubmit(formData: IntegrationFormFields): Promise { const labels = labelsRef.current?.getValue(); diff --git a/grafana-plugin/src/containers/ServiceNowConfigDrawer/ServiceNowConfigDrawer.tsx b/grafana-plugin/src/containers/ServiceNowConfigDrawer/ServiceNowConfigDrawer.tsx index 6e88a07f..8ea3c7be 100644 --- a/grafana-plugin/src/containers/ServiceNowConfigDrawer/ServiceNowConfigDrawer.tsx +++ b/grafana-plugin/src/containers/ServiceNowConfigDrawer/ServiceNowConfigDrawer.tsx @@ -4,7 +4,6 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { Drawer, Field, HorizontalGroup, Input, useStyles2, Button } from '@grafana/ui'; import { observer } from 'mobx-react'; -import { parseUrl } from 'query-string'; import { Controller, FormProvider, useForm } from 'react-hook-form'; import { ActionKey } from 'models/loader/action-keys'; @@ -12,6 +11,7 @@ import { ApiSchemas } from 'network/oncall-api/api.types'; import { useCurrentIntegration } from 'pages/integration/OutgoingTab/OutgoingTab.hooks'; import { useStore } from 'state/useStore'; import { useIsLoading } from 'utils/hooks'; +import { validateURL } from 'utils/string'; import { OmitReadonlyMembers } from 'utils/types'; import { openNotification } from 'utils/utils'; @@ -130,10 +130,6 @@ export const ServiceNowConfigDrawer: React.FC ); - function validateURL(urlFieldValue: string): string | boolean { - return !parseUrl(urlFieldValue) ? 'Instance URL is invalid' : true; - } - async function onFormSubmit(formData: FormFields): Promise { const data: OmitReadonlyMembers = { ...currentIntegration, diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index d3c6bdd3..6c4a60c7 100644 --- a/grafana-plugin/src/pages/incident/Incident.tsx +++ b/grafana-plugin/src/pages/incident/Incident.tsx @@ -20,6 +20,7 @@ import { withTheme2, useStyles2, } from '@grafana/ui'; +import Linkify from 'linkify-react'; import { observer } from 'mobx-react'; import moment from 'moment-timezone'; import CopyToClipboard from 'react-copy-to-clipboard'; @@ -517,7 +518,6 @@ class _IncidentPage extends React.Component { const { store, - history, match: { params: { id }, }, @@ -569,7 +569,17 @@ class _IncidentPage extends React.Component )} - {reactStringReplace(item.action, /\{\{([^}]+)\}\}/g, this.getPlaceholderReplaceFn(item, history))} + ( + + {content} + + ), + }} + > + {this.replaceTextInResolutionNote(item)} + {moment(item.created_at).format('MMM DD, YYYY HH:mm:ss Z')} @@ -636,17 +646,14 @@ class _IncidentPage extends React.Component { + getPlaceholderReplaceFn = (entity: TimeLineItem) => { return (match: string) => { switch (match) { case 'author': return ( - history.push(`${PLUGIN_ROOT}/users/${entity?.author?.pk}`)} - style={{ textDecoration: 'underline', cursor: 'pointer' }} - > - {entity.author?.username} - + + {entity.author?.username} + ); default: return '{{' + match + '}}'; @@ -654,6 +661,9 @@ class _IncidentPage extends React.Component + reactStringReplace(item.action, /\{\{([^}]+)\}\}/g, this.getPlaceholderReplaceFn(item)); + getOnActionButtonClick = (incidentId: ApiSchemas['AlertGroup']['pk'], action: AlertAction) => { const { store } = this.props; diff --git a/grafana-plugin/src/utils/string.ts b/grafana-plugin/src/utils/string.ts index 212b36b3..bc231dea 100644 --- a/grafana-plugin/src/utils/string.ts +++ b/grafana-plugin/src/utils/string.ts @@ -1,3 +1,5 @@ +import { parseURL } from './url'; + // Truncate a string to a given maximum length, adding ellipsis if it was truncated. export function truncateTitle(title: string, length: number): string { if (title.length <= length) { @@ -24,4 +26,6 @@ export const safeJSONStringify = (value: unknown) => { } }; -export const VALID_URL_PATTERN = /(http|https)\:\/\/.+?\..+/; +export function validateURL(urlFieldValue: string): string | boolean { + return !parseURL(urlFieldValue) ? 'URL is invalid' : true; +} diff --git a/grafana-plugin/yarn.lock b/grafana-plugin/yarn.lock index 827b6c98..24a21c2c 100644 --- a/grafana-plugin/yarn.lock +++ b/grafana-plugin/yarn.lock @@ -9582,6 +9582,16 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== +linkify-react@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/linkify-react/-/linkify-react-4.1.3.tgz#461d348b4bdab3fcd0452ae1b5bbc22536395b97" + integrity sha512-rhI3zM/fxn5BfRPHfi4r9N7zgac4vOIxub1wHIWXLA5ENTMs+BGaIaFO1D1PhmxgwhIKmJz3H7uCP0Dg5JwSlA== + +linkifyjs@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.1.3.tgz#0edbc346428a7390a23ea2e5939f76112c9ae07f" + integrity sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg== + lint-staged@^10.2.11: version "10.5.4" resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.5.4.tgz#cd153b5f0987d2371fc1d2847a409a2fe705b665" @@ -13459,16 +13469,7 @@ string-template@~0.2.1: resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -13586,7 +13587,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -13607,13 +13608,6 @@ strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -14954,7 +14948,8 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: + name wrap-ansi-cjs version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -14972,15 +14967,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"