From bbb3648b864064c73d557c60ce46b5c5de88bec1 Mon Sep 17 00:00:00 2001 From: Lukasz Mierzwa Date: Fri, 6 Mar 2026 16:14:03 +0000 Subject: [PATCH] fix(ui): drop qs --- ui/package-lock.json | 24 ---------- ui/package.json | 2 - .../SilenceMatch/MatchCounter.test.tsx | 14 +++--- .../SilencePreview/index.test.tsx | 4 +- ui/src/Stores/AlertStore.ts | 47 +++++++++---------- 5 files changed, 32 insertions(+), 59 deletions(-) diff --git a/ui/package-lock.json b/ui/package-lock.json index 92eae091c..b9733cfb3 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -33,7 +33,6 @@ "mobx-react-lite": "4.1.1", "mobx-stored": "1.1.0", "promise-retry": "2.0.1", - "qs": "6.15.0", "react": "19.2.4", "react-cool-dimensions": "3.0.1", "react-day-picker": "9.14.0", @@ -65,7 +64,6 @@ "@types/lodash.uniqueid": "4.0.9", "@types/node": "25.3.5", "@types/promise-retry": "1.1.6", - "@types/qs": "6.14.0", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "@typescript-eslint/eslint-plugin": "8.56.1", @@ -4730,13 +4728,6 @@ "@types/retry": "*" } }, - "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", @@ -10936,21 +10927,6 @@ ], "license": "MIT" }, - "node_modules/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", diff --git a/ui/package.json b/ui/package.json index a3bbe4c38..03df94c6a 100644 --- a/ui/package.json +++ b/ui/package.json @@ -29,7 +29,6 @@ "mobx-react-lite": "4.1.1", "mobx-stored": "1.1.0", "promise-retry": "2.0.1", - "qs": "6.15.0", "react": "19.2.4", "react-cool-dimensions": "3.0.1", "react-day-picker": "9.14.0", @@ -61,7 +60,6 @@ "@types/lodash.uniqueid": "4.0.9", "@types/node": "25.3.5", "@types/promise-retry": "1.1.6", - "@types/qs": "6.14.0", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "@typescript-eslint/eslint-plugin": "8.56.1", diff --git a/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.test.tsx b/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.test.tsx index 883eb6b3d..4fbf5f48b 100644 --- a/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.test.tsx +++ b/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.test.tsx @@ -146,7 +146,7 @@ describe("", () => { renderMatchCounter(); expect( (useFetchGet as jest.MockedFunction).mock.calls[0][0], - ).toBe("./alertList.json?q=foo%3D~%5Ebar%24"); + ).toBe("./alertList.json?q=foo%3D%7E%5Ebar%24"); }); it("sends correct query string for a 'foo=(x)' matcher with wasCreated=true & isRegex=false", () => { @@ -168,7 +168,7 @@ describe("", () => { renderMatchCounter(); expect( (useFetchGet as jest.MockedFunction).mock.calls[0][0], - ).toBe("./alertList.json?q=foo%3D~%5E%28x%29%24"); + ).toBe("./alertList.json?q=foo%3D%7E%5E%28x%29%24"); }); it("sends correct query string for a 'foo=(x)' matcher with wasCreated=false & isRegex=false", () => { @@ -190,7 +190,7 @@ describe("", () => { renderMatchCounter(); expect( (useFetchGet as jest.MockedFunction).mock.calls[0][0], - ).toBe("./alertList.json?q=foo%3D~%5E%5C%28x%5C%29%24"); + ).toBe("./alertList.json?q=foo%3D%7E%5E%5C%28x%5C%29%24"); }); it("sends correct query string for a 'foo=~(bar|baz)' matcher", () => { @@ -200,7 +200,7 @@ describe("", () => { renderMatchCounter(); expect( (useFetchGet as jest.MockedFunction).mock.calls[0][0], - ).toBe("./alertList.json?q=foo%3D~%5E%28bar%7Cbaz%29%24"); + ).toBe("./alertList.json?q=foo%3D%7E%5E%28bar%7Cbaz%29%24"); }); it("selecting one Alertmanager instance appends it to the filters", () => { @@ -213,7 +213,9 @@ describe("", () => { renderMatchCounter(); expect( (useFetchGet as jest.MockedFunction).mock.calls[0][0], - ).toBe("./alertList.json?q=foo%3Dbar&q=%40alertmanager%3D~%5E%28am1%29%24"); + ).toBe( + "./alertList.json?q=foo%3Dbar&q=%40alertmanager%3D%7E%5E%28am1%29%24", + ); }); it("selecting two Alertmanager instances appends it correctly to the filters", () => { @@ -231,7 +233,7 @@ describe("", () => { expect( (useFetchGet as jest.MockedFunction).mock.calls[0][0], ).toBe( - "./alertList.json?q=foo%3Dbar&q=%40alertmanager%3D~%5E%28am1%7Cam2%29%24", + "./alertList.json?q=foo%3Dbar&q=%40alertmanager%3D%7E%5E%28am1%7Cam2%29%24", ); }); }); diff --git a/ui/src/Components/SilenceModal/SilencePreview/index.test.tsx b/ui/src/Components/SilenceModal/SilencePreview/index.test.tsx index be096cbcf..78e23e22b 100644 --- a/ui/src/Components/SilenceModal/SilencePreview/index.test.tsx +++ b/ui/src/Components/SilenceModal/SilencePreview/index.test.tsx @@ -47,7 +47,7 @@ describe("", () => { ]); renderSilencePreview(); expect(useFetchGet).toHaveBeenCalledWith( - "./alertList.json?q=foo%3Dbar&q=%40alertmanager%3D~%5E%28amValue%29%24", + "./alertList.json?q=foo%3Dbar&q=%40alertmanager%3D%7E%5E%28amValue%29%24", ); }); @@ -57,7 +57,7 @@ describe("", () => { ]); renderSilencePreview(); expect(useFetchGet).toHaveBeenCalledWith( - "./alertList.json?q=foo%3Dbar&q=%40alertmanager%3D~%5E%28am1%7Cam2%29%24", + "./alertList.json?q=foo%3Dbar&q=%40alertmanager%3D%7E%5E%28am1%7Cam2%29%24", ); }); diff --git a/ui/src/Stores/AlertStore.ts b/ui/src/Stores/AlertStore.ts index ca1c02fca..560eab64b 100644 --- a/ui/src/Stores/AlertStore.ts +++ b/ui/src/Stores/AlertStore.ts @@ -2,8 +2,6 @@ import { observable, action, computed, toJS } from "mobx"; import throttle from "lodash.throttle"; -import qs from "qs"; - import { FetchGet } from "Common/Fetch"; import type { APIAlertmanagerUpstreamT, @@ -18,23 +16,15 @@ import type { AlertsRequestT, } from "Models/APITypes"; -const QueryStringEncodeOptions = { - encodeValuesOnly: true, // don't encode q[] - indices: false, // go-gin doesn't support parsing q[0]=foo&q[1]=bar -}; - function FormatAlertsQ(filters: string[]): string { - return qs.stringify({ q: filters }, QueryStringEncodeOptions); + return new URLSearchParams(filters.map((f) => ["q", f])).toString(); } // generate URL for the UI with a set of filters function FormatAPIFilterQuery(filters: string[]): string { - return qs.stringify( - Object.assign(DecodeLocationSearch(window.location.search).params, { - q: filters, - }), - QueryStringEncodeOptions, - ); + const params = DecodeLocationSearch(window.location.search).params; + const merged = { ...params, q: filters || [] }; + return new URLSearchParams(merged.q.map((f) => ["q", f])).toString(); } // format URI for react UI -> Go backend requests @@ -46,6 +36,7 @@ function FormatBackendURI(path: string): string { // and decodes it into a dict with some extra metadata interface QueryParamsT { q: string[]; + m?: string; } interface DecodeLocationSearchReturnT { params: QueryParamsT; @@ -55,26 +46,32 @@ function DecodeLocationSearch( searchString: string, ): DecodeLocationSearchReturnT { let defaultsUsed = true; - let params: QueryParamsT = { q: [] }; + const params: QueryParamsT = { q: [] }; if (searchString !== "") { - const parsed = qs.parse(searchString.split("?")[1]) as { - [key: string]: string | string[]; - }; - params = Object.assign(params, parsed); + const usp = new URLSearchParams(searchString.split("?")[1]); + const mValue = usp.get("m"); + if (mValue !== null) { + params.m = mValue; + } + const qValues = [...usp.getAll("q"), ...usp.getAll("q[]")]; + let parsedQ: string | string[] | undefined; + if (qValues.length > 0) { + parsedQ = qValues.length === 1 ? qValues[0] : qValues; + } - if (parsed.q !== undefined) { + if (parsedQ !== undefined) { defaultsUsed = false; - if (parsed.q === "") { + if (parsedQ === "") { params.q = []; - } else if (Array.isArray(parsed.q)) { + } else if (Array.isArray(parsedQ)) { // first filter out duplicates // then filter out empty strings, so 'q=' doesn't end up [""] but rather [] - params.q = parsed.q - .filter((v: string, i: number) => parsed.q.indexOf(v) === i) + params.q = parsedQ + .filter((v: string, i: number) => parsedQ.indexOf(v) === i) .filter((v: string) => v !== ""); } else { - params.q = [parsed.q]; + params.q = [parsedQ]; } } }