mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Fix incorrect decoding for alert rules with % characters (#76148)
* fixes incorrect decoding for alert rules with % characters * Patch history package to stop decoding path * Rename rule id extraction function, update tests * Fix patch file * Fix copyFrom parameter encoding * Fix conflict commit * Fix test --------- Co-authored-by: Konrad Lalik <konrad.lalik@grafana.com> Co-authored-by: Sonia Aguilar <soniaaguilarpeiron@gmail.com>
This commit is contained in:
parent
61cb26711e
commit
390408b1ce
63
.yarn/patches/history-npm-4.10.1-ee217563ae.patch
Normal file
63
.yarn/patches/history-npm-4.10.1-ee217563ae.patch
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
diff --git a/cjs/history.js b/cjs/history.js
|
||||||
|
index fcd8ebab613c6d87b9ac824feb30ab1080cf0ef2..4df20d5cb2f9ba5fc8777899aada53f49399560b 100644
|
||||||
|
--- a/cjs/history.js
|
||||||
|
+++ b/cjs/history.js
|
||||||
|
@@ -103,16 +103,6 @@ function createLocation(path, state, key, currentLocation) {
|
||||||
|
if (state !== undefined && location.state === undefined) location.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
- try {
|
||||||
|
- location.pathname = decodeURI(location.pathname);
|
||||||
|
- } catch (e) {
|
||||||
|
- if (e instanceof URIError) {
|
||||||
|
- throw new URIError('Pathname "' + location.pathname + '" could not be decoded. ' + 'This is likely caused by an invalid percent-encoding.');
|
||||||
|
- } else {
|
||||||
|
- throw e;
|
||||||
|
- }
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
if (key) location.key = key;
|
||||||
|
|
||||||
|
if (currentLocation) {
|
||||||
|
diff --git a/esm/history.js b/esm/history.js
|
||||||
|
index df67820fe3eed558c44fca07a82b0cd409d46720..e0e0d4f69a407e8de782b3fdf8297d42708e110a 100644
|
||||||
|
--- a/esm/history.js
|
||||||
|
+++ b/esm/history.js
|
||||||
|
@@ -80,16 +80,6 @@ function createLocation(path, state, key, currentLocation) {
|
||||||
|
if (state !== undefined && location.state === undefined) location.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
- try {
|
||||||
|
- location.pathname = decodeURI(location.pathname);
|
||||||
|
- } catch (e) {
|
||||||
|
- if (e instanceof URIError) {
|
||||||
|
- throw new URIError('Pathname "' + location.pathname + '" could not be decoded. ' + 'This is likely caused by an invalid percent-encoding.');
|
||||||
|
- } else {
|
||||||
|
- throw e;
|
||||||
|
- }
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
if (key) location.key = key;
|
||||||
|
|
||||||
|
if (currentLocation) {
|
||||||
|
diff --git a/umd/history.js b/umd/history.js
|
||||||
|
index 80e4ff66c44a2a71d4f842cc05a252e48dd18e9a..f8f4901be95e48c66f5626fbf051747a2ffbe41d 100644
|
||||||
|
--- a/umd/history.js
|
||||||
|
+++ b/umd/history.js
|
||||||
|
@@ -207,16 +207,6 @@
|
||||||
|
if (state !== undefined && location.state === undefined) location.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
- try {
|
||||||
|
- location.pathname = decodeURI(location.pathname);
|
||||||
|
- } catch (e) {
|
||||||
|
- if (e instanceof URIError) {
|
||||||
|
- throw new URIError('Pathname "' + location.pathname + '" could not be decoded. ' + 'This is likely caused by an invalid percent-encoding.');
|
||||||
|
- } else {
|
||||||
|
- throw e;
|
||||||
|
- }
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
if (key) location.key = key;
|
||||||
|
|
||||||
|
if (currentLocation) {
|
@ -1,8 +1,6 @@
|
|||||||
{
|
{
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"useWorkspaces": true,
|
"useWorkspaces": true,
|
||||||
"packages": [
|
"packages": ["packages/*"],
|
||||||
"packages/*"
|
|
||||||
],
|
|
||||||
"version": "10.3.0-pre"
|
"version": "10.3.0-pre"
|
||||||
}
|
}
|
||||||
|
@ -430,7 +430,9 @@
|
|||||||
"semver@7.3.4": "7.5.4",
|
"semver@7.3.4": "7.5.4",
|
||||||
"slate-dev-environment@^0.2.2": "patch:slate-dev-environment@npm:0.2.5#.yarn/patches/slate-dev-environment-npm-0.2.5-9aeb7da7b5.patch",
|
"slate-dev-environment@^0.2.2": "patch:slate-dev-environment@npm:0.2.5#.yarn/patches/slate-dev-environment-npm-0.2.5-9aeb7da7b5.patch",
|
||||||
"react-split-pane@0.1.92": "patch:react-split-pane@npm:0.1.92#.yarn/patches/react-split-pane-npm-0.1.92-93dbf51dff.patch",
|
"react-split-pane@0.1.92": "patch:react-split-pane@npm:0.1.92#.yarn/patches/react-split-pane-npm-0.1.92-93dbf51dff.patch",
|
||||||
"@storybook/blocks@7.4.5": "patch:@storybook/blocks@npm%3A7.4.5#./.yarn/patches/@storybook-blocks-npm-7.4.5-5a2374564a.patch"
|
"@storybook/blocks@7.4.5": "patch:@storybook/blocks@npm%3A7.4.5#./.yarn/patches/@storybook-blocks-npm-7.4.5-5a2374564a.patch",
|
||||||
|
"history@4.10.1": "patch:history@npm%3A4.10.1#./.yarn/patches/history-npm-4.10.1-ee217563ae.patch",
|
||||||
|
"history@^4.9.0": "patch:history@npm%3A4.10.1#./.yarn/patches/history-npm-4.10.1-ee217563ae.patch"
|
||||||
},
|
},
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"packages": [
|
"packages": [
|
||||||
|
@ -47,7 +47,8 @@ const RuleEditor = ({ match }: RuleEditorProps) => {
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [searchParams] = useURLSearchParams();
|
const [searchParams] = useURLSearchParams();
|
||||||
|
|
||||||
const { id, type } = match.params;
|
const { type } = match.params;
|
||||||
|
const id = ruleId.getRuleIdFromPathname(match.params);
|
||||||
const identifier = ruleId.tryParse(id, true);
|
const identifier = ruleId.tryParse(id, true);
|
||||||
|
|
||||||
const copyFromId = searchParams.get('copyFrom') ?? undefined;
|
const copyFromId = searchParams.get('copyFrom') ?? undefined;
|
||||||
|
@ -68,7 +68,8 @@ const ui = {
|
|||||||
loadingIndicator: byText(/Loading rule/i),
|
loadingIndicator: byText(/Loading rule/i),
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderRuleViewer = async (ruleId?: string) => {
|
const renderRuleViewer = async (ruleId: string) => {
|
||||||
|
locationService.push(`/alerting/grafana/${ruleId}/view`);
|
||||||
render(
|
render(
|
||||||
<TestProvider>
|
<TestProvider>
|
||||||
<RuleViewer {...mockRoute(ruleId)} />
|
<RuleViewer {...mockRoute(ruleId)} />
|
||||||
@ -153,7 +154,7 @@ describe('RuleViewer', () => {
|
|||||||
|
|
||||||
it('should render page with grafana alert', async () => {
|
it('should render page with grafana alert', async () => {
|
||||||
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: false });
|
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: false });
|
||||||
await renderRuleViewer();
|
await renderRuleViewer('test1');
|
||||||
|
|
||||||
expect(screen.getByText(/test alert/i)).toBeInTheDocument();
|
expect(screen.getByText(/test alert/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@ -195,7 +196,7 @@ describe('RuleDetails RBAC', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await renderRuleViewer();
|
await renderRuleViewer('test1');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(ui.actionButtons.edit.get()).toBeInTheDocument();
|
expect(ui.actionButtons.edit.get()).toBeInTheDocument();
|
||||||
@ -215,7 +216,7 @@ describe('RuleDetails RBAC', () => {
|
|||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await renderRuleViewer();
|
await renderRuleViewer('test1');
|
||||||
await user.click(ui.moreButton.get());
|
await user.click(ui.moreButton.get());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
@ -234,7 +235,7 @@ describe('RuleDetails RBAC', () => {
|
|||||||
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false);
|
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await renderRuleViewer();
|
await renderRuleViewer('test1');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@ -256,7 +257,7 @@ describe('RuleDetails RBAC', () => {
|
|||||||
.mockImplementation((action) => action === AccessControlAction.AlertingInstanceCreate);
|
.mockImplementation((action) => action === AccessControlAction.AlertingInstanceCreate);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await renderRuleViewer();
|
await renderRuleViewer('test1');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@ -275,7 +276,7 @@ describe('RuleDetails RBAC', () => {
|
|||||||
|
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
|
|
||||||
await renderRuleViewer();
|
await renderRuleViewer('test1');
|
||||||
await user.click(ui.moreButton.get());
|
await user.click(ui.moreButton.get());
|
||||||
|
|
||||||
expect(ui.moreButtons.duplicate.get()).toBeInTheDocument();
|
expect(ui.moreButtons.duplicate.get()).toBeInTheDocument();
|
||||||
@ -293,7 +294,7 @@ describe('RuleDetails RBAC', () => {
|
|||||||
grantUserPermissions([AlertingRuleRead, AlertingRuleUpdate, AlertingRuleDelete]);
|
grantUserPermissions([AlertingRuleRead, AlertingRuleUpdate, AlertingRuleDelete]);
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
|
|
||||||
await renderRuleViewer();
|
await renderRuleViewer('test1');
|
||||||
await user.click(ui.moreButton.get());
|
await user.click(ui.moreButton.get());
|
||||||
|
|
||||||
expect(ui.moreButtons.duplicate.query()).not.toBeInTheDocument();
|
expect(ui.moreButtons.duplicate.query()).not.toBeInTheDocument();
|
||||||
@ -321,7 +322,7 @@ describe('RuleDetails RBAC', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await renderRuleViewer();
|
await renderRuleViewer('test1');
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(ui.actionButtons.edit.query()).toBeInTheDocument();
|
expect(ui.actionButtons.edit.query()).toBeInTheDocument();
|
||||||
@ -341,7 +342,7 @@ describe('RuleDetails RBAC', () => {
|
|||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await renderRuleViewer();
|
await renderRuleViewer('test1');
|
||||||
await user.click(ui.moreButton.get());
|
await user.click(ui.moreButton.get());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
@ -43,14 +43,14 @@ export function RuleViewer({ match }: RuleViewerProps) {
|
|||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const [expandQuery, setExpandQuery] = useToggle(false);
|
const [expandQuery, setExpandQuery] = useToggle(false);
|
||||||
|
|
||||||
const { id } = match.params;
|
|
||||||
const identifier = useMemo(() => {
|
const identifier = useMemo(() => {
|
||||||
|
const id = ruleId.getRuleIdFromPathname(match.params);
|
||||||
if (!id) {
|
if (!id) {
|
||||||
throw new Error('Rule ID is required');
|
throw new Error('Rule ID is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
return ruleId.parse(id, true);
|
return ruleId.parse(id, true);
|
||||||
}, [id]);
|
}, [match.params]);
|
||||||
|
|
||||||
const { loading, error, result: rule } = useCombinedRule({ ruleIdentifier: identifier });
|
const { loading, error, result: rule } = useCombinedRule({ ruleIdentifier: identifier });
|
||||||
|
|
||||||
|
@ -34,9 +34,9 @@ enum Tabs {
|
|||||||
// figure out why we needed <AlertingPageWrapper>
|
// figure out why we needed <AlertingPageWrapper>
|
||||||
// add provisioning and federation stuff back in
|
// add provisioning and federation stuff back in
|
||||||
const RuleViewer = ({ match }: RuleViewerProps) => {
|
const RuleViewer = ({ match }: RuleViewerProps) => {
|
||||||
const { id } = match.params;
|
|
||||||
const [activeTab, setActiveTab] = useState<Tabs>(Tabs.Instances);
|
const [activeTab, setActiveTab] = useState<Tabs>(Tabs.Instances);
|
||||||
|
|
||||||
|
const id = ruleId.getRuleIdFromPathname(match.params);
|
||||||
const identifier = useMemo(() => {
|
const identifier = useMemo(() => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
throw new Error('Rule ID is required');
|
throw new Error('Rule ID is required');
|
||||||
|
@ -22,7 +22,7 @@ export function RedirectToCloneRule({ identifier, isProvisioned, onDismiss }: Co
|
|||||||
const [stage, setStage] = useState<'redirect' | 'confirm'>(isProvisioned ? 'confirm' : 'redirect');
|
const [stage, setStage] = useState<'redirect' | 'confirm'>(isProvisioned ? 'confirm' : 'redirect');
|
||||||
|
|
||||||
if (stage === 'redirect') {
|
if (stage === 'redirect') {
|
||||||
const cloneUrl = `/alerting/new?copyFrom=${ruleId.stringifyIdentifier(identifier)}`;
|
const cloneUrl = `/alerting/new?copyFrom=${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}`;
|
||||||
return <Redirect to={cloneUrl} push />;
|
return <Redirect to={cloneUrl} push />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { renderHook } from '@testing-library/react-hooks';
|
||||||
|
|
||||||
import { RuleIdentifier } from 'app/types/unified-alerting';
|
import { RuleIdentifier } from 'app/types/unified-alerting';
|
||||||
import {
|
import {
|
||||||
GrafanaAlertStateDecision,
|
GrafanaAlertStateDecision,
|
||||||
@ -7,7 +9,7 @@ import {
|
|||||||
RulerRecordingRuleDTO,
|
RulerRecordingRuleDTO,
|
||||||
} from 'app/types/unified-alerting-dto';
|
} from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
import { hashRulerRule, parse, stringifyIdentifier } from './rule-id';
|
import { hashRulerRule, parse, stringifyIdentifier, getRuleIdFromPathname } from './rule-id';
|
||||||
|
|
||||||
describe('hashRulerRule', () => {
|
describe('hashRulerRule', () => {
|
||||||
it('should not hash unknown rule types', () => {
|
it('should not hash unknown rule types', () => {
|
||||||
@ -114,3 +116,13 @@ describe('hashRulerRule', () => {
|
|||||||
expect(() => parse('foo$bar$baz', false)).toThrow(/failed to parse/i);
|
expect(() => parse('foo$bar$baz', false)).toThrow(/failed to parse/i);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('useRuleIdFromPathname', () => {
|
||||||
|
it('should return undefined when there is no id in params', () => {
|
||||||
|
const { result } = renderHook(() => {
|
||||||
|
getRuleIdFromPathname({ id: undefined });
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current).toBe(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import { nth } from 'lodash';
|
||||||
|
|
||||||
|
import { locationService } from '@grafana/runtime';
|
||||||
import { CombinedRule, Rule, RuleIdentifier, RuleWithLocation } from 'app/types/unified-alerting';
|
import { CombinedRule, Rule, RuleIdentifier, RuleWithLocation } from 'app/types/unified-alerting';
|
||||||
import { Annotations, Labels, RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
import { Annotations, Labels, RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
@ -244,3 +247,17 @@ function hashLabelsOrAnnotations(item: Labels | Annotations | undefined): string
|
|||||||
export function ruleIdentifierToRuleSourceName(identifier: RuleIdentifier): string {
|
export function ruleIdentifierToRuleSourceName(identifier: RuleIdentifier): string {
|
||||||
return isGrafanaRuleIdentifier(identifier) ? GRAFANA_RULES_SOURCE_NAME : identifier.ruleSourceName;
|
return isGrafanaRuleIdentifier(identifier) ? GRAFANA_RULES_SOURCE_NAME : identifier.ruleSourceName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DO NOT USE REACT-ROUTER HOOKS FOR THIS CODE
|
||||||
|
// React-router's useLocation/useParams/props.match are broken and don't preserve original param values when parsing location
|
||||||
|
// so, they cannot be used to parse name and sourceName path params
|
||||||
|
// React-router messes the pathname up resulting in a string that is neither encoded nor decoded
|
||||||
|
// Relevant issue: https://github.com/remix-run/history/issues/505#issuecomment-453175833
|
||||||
|
// It was probably fixed in React-Router v6
|
||||||
|
type PathWithOptionalID = { id?: string };
|
||||||
|
export function getRuleIdFromPathname(params: PathWithOptionalID): string | undefined {
|
||||||
|
const { pathname = '' } = locationService.getLocation();
|
||||||
|
const { id } = params;
|
||||||
|
|
||||||
|
return id ? nth(pathname.split('/'), -2) : undefined;
|
||||||
|
}
|
||||||
|
16
yarn.lock
16
yarn.lock
@ -18336,7 +18336,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"history@npm:4.10.1, history@npm:^4.9.0":
|
"history@npm:4.10.1":
|
||||||
version: 4.10.1
|
version: 4.10.1
|
||||||
resolution: "history@npm:4.10.1"
|
resolution: "history@npm:4.10.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -18359,6 +18359,20 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"history@patch:history@npm%3A4.10.1#./.yarn/patches/history-npm-4.10.1-ee217563ae.patch::locator=grafana%40workspace%3A.":
|
||||||
|
version: 4.10.1
|
||||||
|
resolution: "history@patch:history@npm%3A4.10.1#./.yarn/patches/history-npm-4.10.1-ee217563ae.patch::version=4.10.1&hash=b8466f&locator=grafana%40workspace%3A."
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime": ^7.1.2
|
||||||
|
loose-envify: ^1.2.0
|
||||||
|
resolve-pathname: ^3.0.0
|
||||||
|
tiny-invariant: ^1.0.2
|
||||||
|
tiny-warning: ^1.0.0
|
||||||
|
value-equal: ^1.0.1
|
||||||
|
checksum: bd33efe9eb241b79658ecea1b622a0eebeba70e280aceb439e988ff1d6859016a35b922f2ebcfca7dc07804e4af54ec2ecad3778d42d465e470d553e49b74e50
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"hoist-non-react-statics@npm:3.3.2, hoist-non-react-statics@npm:^3.1.0, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2":
|
"hoist-non-react-statics@npm:3.3.2, hoist-non-react-statics@npm:^3.1.0, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2":
|
||||||
version: 3.3.2
|
version: 3.3.2
|
||||||
resolution: "hoist-non-react-statics@npm:3.3.2"
|
resolution: "hoist-non-react-statics@npm:3.3.2"
|
||||||
|
Loading…
Reference in New Issue
Block a user