make links clickable in resolution notes (#4572)

# What this PR does
make links clickable in resolution notes


![image](https://github.com/grafana/oncall/assets/12073649/ca0b425f-85b9-4714-a73f-a384e032ee21)


## Which issue(s) this PR closes
Closes https://github.com/grafana/oncall/issues/4231
Closes https://github.com/grafana/oncall/issues/505

<!--
*Note*: if you have more than one GitHub issue that this PR closes, be
sure to preface
each issue link with a [closing
keyword](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests#linking-a-pull-request-to-an-issue).
This ensures that the issue(s) are auto-closed once the PR has been
merged.
-->

## Checklist

- [ ] Unit, integration, and e2e (if applicable) tests updated
- [x] Documentation added (or `pr:no public docs` PR label added if not
required)
- [x] Added the relevant release notes label (see labels prefixed w/
`release:`). These labels dictate how your PR will
    show up in the autogenerated release notes.
This commit is contained in:
Dominik Broj 2024-06-24 21:08:50 +02:00 committed by GitHub
parent 48b7eca26d
commit c39dd8b4cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 42 additions and 48 deletions

View file

@ -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",

View file

@ -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<void> {
const labels = labelsRef.current?.getValue();

View file

@ -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<ServiceNowConfigurationDrawerProps
</>
);
function validateURL(urlFieldValue: string): string | boolean {
return !parseUrl(urlFieldValue) ? 'Instance URL is invalid' : true;
}
async function onFormSubmit(formData: FormFields): Promise<void> {
const data: OmitReadonlyMembers<ApiSchemas['AlertReceiveChannel']> = {
...currentIntegration,

View file

@ -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<IncidentPageProps, IncidentPageState
renderTimeline = () => {
const {
store,
history,
match: {
params: { id },
},
@ -569,7 +569,17 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
</Text>
)}
<Text type="primary">
{reactStringReplace(item.action, /\{\{([^}]+)\}\}/g, this.getPlaceholderReplaceFn(item, history))}
<Linkify
options={{
render: ({ attributes, content }) => (
<a {...attributes} rel="noreferrer noopener" target="_blank">
<Text underline>{content}</Text>
</a>
),
}}
>
{this.replaceTextInResolutionNote(item)}
</Linkify>
</Text>
<Text type="secondary" size="small">
{moment(item.created_at).format('MMM DD, YYYY HH:mm:ss Z')}
@ -636,17 +646,14 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
await this.update();
};
getPlaceholderReplaceFn = (entity: any, history) => {
getPlaceholderReplaceFn = (entity: TimeLineItem) => {
return (match: string) => {
switch (match) {
case 'author':
return (
<span
onClick={() => history.push(`${PLUGIN_ROOT}/users/${entity?.author?.pk}`)}
style={{ textDecoration: 'underline', cursor: 'pointer' }}
>
{entity.author?.username}
</span>
<a href={`${PLUGIN_ROOT}/users/${entity?.author?.pk}`} target="_blank" rel="noopener noreferrer">
<Text underline>{entity.author?.username}</Text>
</a>
);
default:
return '{{' + match + '}}';
@ -654,6 +661,9 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
};
};
replaceTextInResolutionNote = (item: TimeLineItem) =>
reactStringReplace(item.action, /\{\{([^}]+)\}\}/g, this.getPlaceholderReplaceFn(item));
getOnActionButtonClick = (incidentId: ApiSchemas['AlertGroup']['pk'], action: AlertAction) => {
const { store } = this.props;

View file

@ -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;
}

View file

@ -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"