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