diff --git a/ui/src/Common/Fetch.js b/ui/src/Common/Fetch.js index 6d4a8f068..a62de42a0 100644 --- a/ui/src/Common/Fetch.js +++ b/ui/src/Common/Fetch.js @@ -1,9 +1,28 @@ import merge from "lodash.merge"; -const FetchWithCredentials = async (uri, options) => +const CommonOptions = { + credentials: "include", + redirect: "follow" +}; + +const FetchGet = async (uri, options) => await fetch( uri, - merge({}, { credentials: "include", redirect: "follow" }, options) + merge( + {}, + { + method: "GET", + mode: "no-cors" + }, + CommonOptions, + options + ) ); -export { FetchWithCredentials }; +const FetchPost = async (uri, options) => + await fetch(uri, merge({}, { method: "POST" }, CommonOptions, options)); + +const FetchDelete = async (uri, options) => + await fetch(uri, merge({}, { method: "DELETE" }, CommonOptions, options)); + +export { CommonOptions, FetchGet, FetchPost, FetchDelete }; diff --git a/ui/src/Common/Fetch.test.js b/ui/src/Common/Fetch.test.js index ed244d32a..7218c76c9 100644 --- a/ui/src/Common/Fetch.test.js +++ b/ui/src/Common/Fetch.test.js @@ -1,4 +1,6 @@ -import { FetchWithCredentials } from "./Fetch"; +import { CommonOptions, FetchGet, FetchPost, FetchDelete } from "./Fetch"; + +import merge from "lodash.merge"; beforeEach(() => { fetch.resetMocks(); @@ -8,37 +10,53 @@ afterEach(() => { jest.restoreAllMocks(); }); -describe("FetchWithCredentials", () => { - it("fetch passes '{credentials: include}' to all requests", async () => { - const request = FetchWithCredentials("http://example.com", {}); - await expect(request).resolves.toBeUndefined(); - expect(fetch).toHaveBeenCalledWith("http://example.com", { - credentials: "include", - redirect: "follow" - }); - }); +describe("Fetch", () => { + const tests = { + FetchGet: FetchGet, + FetchPost: FetchPost, + FetchDelete: FetchDelete + }; - it("custom keys are merged with defaults", async () => { - const request = FetchWithCredentials("http://example.com", { - foo: "bar" - }); - await expect(request).resolves.toBeUndefined(); - expect(fetch).toHaveBeenCalledWith("http://example.com", { - credentials: "include", - redirect: "follow", - foo: "bar" - }); - }); + const methodOptions = { + FetchGet: { method: "GET", mode: "no-cors" }, + FetchPost: { method: "POST" }, + FetchDelete: { method: "DELETE" } + }; - it("custom credentials are used when passed", async () => { - const request = FetchWithCredentials("http://example.com", { - credentials: "none", - redirect: "follow" + for (const [name, func] of Object.entries(tests)) { + it(`${name}: passes '{credentials: include}' to all requests`, async () => { + const request = func("http://example.com", {}); + await expect(request).resolves.toBeUndefined(); + expect(fetch).toHaveBeenCalledWith( + "http://example.com", + merge({}, CommonOptions, methodOptions[name]) + ); }); - await expect(request).resolves.toBeUndefined(); - expect(fetch).toHaveBeenCalledWith("http://example.com", { - credentials: "none", - redirect: "follow" + + it(`${name}: custom keys are merged with defaults`, async () => { + const request = func("http://example.com", { + foo: "bar" + }); + await expect(request).resolves.toBeUndefined(); + expect(fetch).toHaveBeenCalledWith( + "http://example.com", + merge({}, CommonOptions, methodOptions[name], { foo: "bar" }) + ); }); - }); + + it(`${name}: custom credentials are used when passed`, async () => { + const request = func("http://example.com", { + credentials: "none", + redirect: "follow" + }); + await expect(request).resolves.toBeUndefined(); + expect(fetch).toHaveBeenCalledWith( + "http://example.com", + merge({}, CommonOptions, methodOptions[name], { + credentials: "none", + redirect: "follow" + }) + ); + }); + } }); diff --git a/ui/src/Components/AlertAck/index.js b/ui/src/Components/AlertAck/index.js index 9f517e7af..a6f41c8ac 100644 --- a/ui/src/Components/AlertAck/index.js +++ b/ui/src/Components/AlertAck/index.js @@ -21,7 +21,7 @@ import { MatchersFromGroup, GenerateAlertmanagerSilenceData } from "Stores/SilenceFormStore"; -import { FetchWithCredentials } from "Common/Fetch"; +import { FetchPost } from "Common/Fetch"; import { TooltipWrapper } from "Components/TooltipWrapper"; const SubmitState = Object.freeze({ @@ -148,19 +148,15 @@ const AlertAck = observer( ? `${am.uri}/api/v2/silences` : `${am.uri}/api/v1/silences`; - this.submitState.silencesByCluster[cluster].fetch = FetchWithCredentials( - uri, - { - method: "POST", - body: JSON.stringify( - this.submitState.silencesByCluster[cluster].payload - ), - headers: { - "Content-Type": "application/json", - ...am.headers - } + this.submitState.silencesByCluster[cluster].fetch = FetchPost(uri, { + body: JSON.stringify( + this.submitState.silencesByCluster[cluster].payload + ), + headers: { + "Content-Type": "application/json", + ...am.headers } - ) + }) .then(result => { if (isOpenAPI) { if (result.ok) { diff --git a/ui/src/Components/MainModal/Configuration/SortLabelName.js b/ui/src/Components/MainModal/Configuration/SortLabelName.js index e6fafa75f..8d68d8238 100644 --- a/ui/src/Components/MainModal/Configuration/SortLabelName.js +++ b/ui/src/Components/MainModal/Configuration/SortLabelName.js @@ -7,7 +7,7 @@ import { observer } from "mobx-react"; import Creatable from "react-select/creatable"; import { StaticLabels } from "Common/Query"; -import { FetchWithCredentials } from "Common/Fetch"; +import { FetchGet } from "Common/Fetch"; import { FormatBackendURI } from "Stores/AlertStore"; import { Settings } from "Stores/Settings"; import { ReactSelectStyles } from "Components/MultiSelect"; @@ -34,7 +34,7 @@ const SortLabelName = observer( }); populateNameSuggestions = action(() => { - this.nameSuggestionsFetch = FetchWithCredentials( + this.nameSuggestionsFetch = FetchGet( FormatBackendURI(`labelNames.json`), {} ) diff --git a/ui/src/Components/ManagedSilence/DeleteSilence.js b/ui/src/Components/ManagedSilence/DeleteSilence.js index 829d9e2cc..f26dd0f9f 100644 --- a/ui/src/Components/ManagedSilence/DeleteSilence.js +++ b/ui/src/Components/ManagedSilence/DeleteSilence.js @@ -16,7 +16,7 @@ import { APISilence } from "Models/API"; import { AlertStore, FormatBackendURI, FormatAlertsQ } from "Stores/AlertStore"; import { SilenceFormStore } from "Stores/SilenceFormStore"; import { FormatQuery, QueryOperators, StaticLabels } from "Common/Query"; -import { FetchWithCredentials } from "Common/Fetch"; +import { FetchGet, FetchDelete } from "Common/Fetch"; import { Modal } from "Components/Modal"; import { LabelSetList, @@ -144,7 +144,7 @@ const DeleteSilenceModalContent = observer( FormatQuery(StaticLabels.SilenceID, QueryOperators.Equal, silence.id) ]); - this.previewState.fetch = FetchWithCredentials(alertsURI, {}) + this.previewState.fetch = FetchGet(alertsURI, {}) .then(result => result.json()) .then(result => { this.previewState.groupsToUniqueLabels(Object.values(result.groups)); @@ -175,8 +175,7 @@ const DeleteSilenceModalContent = observer( ? `${alertmanager.uri}/api/v2/silence/${silence.id}` : `${alertmanager.uri}/api/v1/silence/${silence.id}`; - this.deleteState.fetch = FetchWithCredentials(uri, { - method: "DELETE", + this.deleteState.fetch = FetchDelete(uri, { headers: alertmanager.headers }) .then(result => { diff --git a/ui/src/Components/NavBar/FilterInput/index.js b/ui/src/Components/NavBar/FilterInput/index.js index 0dc7b5329..a21cffef5 100644 --- a/ui/src/Components/NavBar/FilterInput/index.js +++ b/ui/src/Components/NavBar/FilterInput/index.js @@ -15,7 +15,7 @@ import { faSearch } from "@fortawesome/free-solid-svg-icons/faSearch"; import { AlertStore, FormatBackendURI } from "Stores/AlertStore"; import { Settings } from "Stores/Settings"; import { IsMobile } from "Common/Device"; -import { FetchWithCredentials } from "Common/Fetch"; +import { FetchGet } from "Common/Fetch"; import { FilterInputLabel } from "Components/Labels/FilterInputLabel"; import { AutosuggestTheme } from "./Constants"; import { History } from "./History"; @@ -73,7 +73,7 @@ const FilterInput = observer( onSuggestionsFetchRequested = debounce( action(({ value }) => { if (value !== "") { - this.inputStore.suggestionsFetch = FetchWithCredentials( + this.inputStore.suggestionsFetch = FetchGet( FormatBackendURI(`autocomplete.json?term=${value}`), {} ) diff --git a/ui/src/Components/SilenceModal/Browser/index.js b/ui/src/Components/SilenceModal/Browser/index.js index a3579cac8..6fdbce575 100644 --- a/ui/src/Components/SilenceModal/Browser/index.js +++ b/ui/src/Components/SilenceModal/Browser/index.js @@ -21,7 +21,7 @@ import { faAngleDoubleRight } from "@fortawesome/free-solid-svg-icons/faAngleDou import { AlertStore, FormatBackendURI } from "Stores/AlertStore"; import { SilenceFormStore } from "Stores/SilenceFormStore"; import { Settings } from "Stores/Settings"; -import { FetchWithCredentials } from "Common/Fetch"; +import { FetchGet } from "Common/Fetch"; import { MountFade } from "Components/Animations/MountFade"; import { ManagedSilence } from "Components/ManagedSilence"; @@ -102,7 +102,7 @@ const Browser = observer( }` ); - this.dataSource.fetch = FetchWithCredentials(uri, {}) + this.dataSource.fetch = FetchGet(uri, {}) .then(result => { return result.json(); }) diff --git a/ui/src/Components/SilenceModal/SilenceMatch/LabelNameInput.js b/ui/src/Components/SilenceModal/SilenceMatch/LabelNameInput.js index 8fc6c6a9e..92434d764 100644 --- a/ui/src/Components/SilenceModal/SilenceMatch/LabelNameInput.js +++ b/ui/src/Components/SilenceModal/SilenceMatch/LabelNameInput.js @@ -8,7 +8,7 @@ import { SilenceFormMatcher } from "Models/SilenceForm"; import { MultiSelect } from "Components/MultiSelect"; import { ValidationError } from "Components/MultiSelect/ValidationError"; import { FormatBackendURI } from "Stores/AlertStore"; -import { FetchWithCredentials } from "Common/Fetch"; +import { FetchGet } from "Common/Fetch"; const LabelNameInput = observer( class LabelNameInput extends MultiSelect { @@ -20,7 +20,7 @@ const LabelNameInput = observer( populateNameSuggestions = action(() => { const { matcher } = this.props; - this.nameSuggestionsFetch = FetchWithCredentials( + this.nameSuggestionsFetch = FetchGet( FormatBackendURI(`labelNames.json`), {} ) @@ -45,7 +45,7 @@ const LabelNameInput = observer( populateValueSuggestions = action(() => { const { matcher } = this.props; - this.valueSuggestionsFetch = FetchWithCredentials( + this.valueSuggestionsFetch = FetchGet( FormatBackendURI(`labelValues.json?name=${matcher.name}`), {} ) diff --git a/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.js b/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.js index 8f9d19bc5..b867ebf6e 100644 --- a/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.js +++ b/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.js @@ -14,7 +14,7 @@ import { FormatBackendURI, FormatAlertsQ } from "Stores/AlertStore"; import { SilenceFormStore } from "Stores/SilenceFormStore"; import { SilenceFormMatcher } from "Models/SilenceForm"; import { TooltipWrapper } from "Components/TooltipWrapper"; -import { FetchWithCredentials } from "Common/Fetch"; +import { FetchGet } from "Common/Fetch"; import { MatcherToFilter, AlertManagersToFilter } from "../Matchers"; const MatchCounter = observer( @@ -55,7 +55,7 @@ const MatchCounter = observer( const alertsURI = FormatBackendURI("alerts.json?") + FormatAlertsQ(filters); - this.matchedAlerts.fetch = FetchWithCredentials(alertsURI, {}) + this.matchedAlerts.fetch = FetchGet(alertsURI, {}) .then(result => { return result.json(); }) diff --git a/ui/src/Components/SilenceModal/SilencePreview/index.js b/ui/src/Components/SilenceModal/SilencePreview/index.js index ad7a6d769..c061f5578 100644 --- a/ui/src/Components/SilenceModal/SilencePreview/index.js +++ b/ui/src/Components/SilenceModal/SilencePreview/index.js @@ -16,7 +16,7 @@ import { LabelSetList, GroupListToUniqueLabelsList } from "Components/LabelSetList"; -import { FetchWithCredentials } from "Common/Fetch"; +import { FetchGet } from "Common/Fetch"; import { MatcherToFilter, AlertManagersToFilter } from "../Matchers"; const FetchError = ({ message }) => ( @@ -82,7 +82,7 @@ const SilencePreview = observer( const alertsURI = FormatBackendURI("alerts.json?") + FormatAlertsQ(filters); - this.matchedAlerts.fetch = FetchWithCredentials(alertsURI, {}) + this.matchedAlerts.fetch = FetchGet(alertsURI, {}) .then(result => { return result.json(); }) diff --git a/ui/src/Components/SilenceModal/SilencePreview/index.test.js b/ui/src/Components/SilenceModal/SilencePreview/index.test.js index e4d3ec9ad..66935abc6 100644 --- a/ui/src/Components/SilenceModal/SilencePreview/index.test.js +++ b/ui/src/Components/SilenceModal/SilencePreview/index.test.js @@ -85,11 +85,14 @@ describe("", () => { const tree = MountedSilencePreview(); await expect(tree.instance().matchedAlerts.fetch).resolves.toBeUndefined(); - expect( - fetch - ).toHaveBeenCalledWith( + expect(fetch).toHaveBeenCalledWith( "./alerts.json?q=foo%3Dbar&q=%40alertmanager%3D~%5E%28amValue%29%24", - { credentials: "include", redirect: "follow" } + { + method: "GET", + mode: "no-cors", + credentials: "include", + redirect: "follow" + } ); }); @@ -101,11 +104,14 @@ describe("", () => { const tree = MountedSilencePreview(); await expect(tree.instance().matchedAlerts.fetch).resolves.toBeUndefined(); - expect( - fetch - ).toHaveBeenCalledWith( + expect(fetch).toHaveBeenCalledWith( "./alerts.json?q=foo%3Dbar&q=%40alertmanager%3D~%5E%28am1%7Cam2%29%24", - { credentials: "include", redirect: "follow" } + { + method: "GET", + mode: "no-cors", + credentials: "include", + redirect: "follow" + } ); }); diff --git a/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.js b/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.js index bd19778e9..25354e794 100644 --- a/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.js +++ b/ui/src/Components/SilenceModal/SilenceSubmit/SilenceSubmitProgress.js @@ -13,7 +13,7 @@ import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclama import { APISilenceMatcher } from "Models/API"; import { AlertStore } from "Stores/AlertStore"; -import { FetchWithCredentials } from "Common/Fetch"; +import { FetchPost } from "Common/Fetch"; const SubmitState = Object.freeze({ InProgress: "InProgress", @@ -109,8 +109,7 @@ const SilenceSubmitProgress = observer( ? `${am.uri}/api/v2/silences` : `${am.uri}/api/v1/silences`; - this.submitState.fetch = FetchWithCredentials(uri, { - method: "POST", + this.submitState.fetch = FetchPost(uri, { body: JSON.stringify(payload), headers: { "Content-Type": "application/json", diff --git a/ui/src/Stores/AlertStore.js b/ui/src/Stores/AlertStore.js index 3ec98f539..4d81acb89 100644 --- a/ui/src/Stores/AlertStore.js +++ b/ui/src/Stores/AlertStore.js @@ -8,7 +8,7 @@ import qs from "qs"; import moment from "moment"; -import { FetchWithCredentials } from "Common/Fetch"; +import { FetchGet } from "Common/Fetch"; const QueryStringEncodeOptions = { encodeValuesOnly: true, // don't encode q[] @@ -268,7 +268,7 @@ class AlertStore { `alerts.json?sortOrder=${sortOrder}&sortLabel=${sortLabel}&sortReverse=${sortReverse}&` ) + FormatAPIFilterQuery(this.filters.values.map(f => f.raw)); - return FetchWithCredentials(alertsURI, {}) + return FetchGet(alertsURI, {}) .then(result => { this.status.setProcessing(); return result.json();