Files
pleno-vue/src/services/superuserReleases.js
T
Jeppe Bundgaard ebcd52a019 Add endpoint mode options, release channel rollback handling, and runtime refresh improvements
- Implement `endpoint_mode` (manual/auto) and related configuration in i18n across multiple locales.
- Add new utility methods for channel rollback and runtime confirmation workflows.
- Update unit and E2E tests to cover runtime refresh behavior, channel switching mechanics, and release references.
- Enhance `SessionUser` and related services to improve error handling during release runtime refresh.
- Add data-test attributes for frontend components to support improved test coverage.
2026-05-21 14:47:46 +02:00

253 lines
9.1 KiB
JavaScript

import axios from "axios";
import { API_URL, RELEASE_MANAGER_CONTROL_API_FALLBACK_URLS, RELEASE_MANAGER_CONTROL_API_URL } from "@/config.js";
import { enqueueRequest } from "@/services/requestQueue.js";
export const RELEASE_MANAGER_CONTROL_API_STORAGE_KEY = "release_manager_control_api_url";
const RELEASE_MANAGER_LAST_WORKING_CONTROL_API_STORAGE_KEY = "release_manager_last_working_control_api_url";
const normalizeApiUrl = (value) => {
const url = String(value || "")
.trim()
.replace(/\/+$/, "");
if (!/^https?:\/\//i.test(url)) {
return "";
}
return url;
};
const browserStorage = (name) => {
if (typeof window === "undefined") {
return null;
}
try {
return window[name] || null;
} catch {
return null;
}
};
const getSelectedCustomerNumber = () => {
const stored = browserStorage("localStorage")?.getItem("selected_customer_number");
return stored ? parseInt(stored, 10) : null;
};
const uniqueUrls = (urls) => {
const seen = new Set();
return urls
.map((url) => normalizeApiUrl(url))
.filter((url) => {
if (!url || seen.has(url)) {
return false;
}
seen.add(url);
return true;
});
};
const releaseManagerLocalStorage = () => browserStorage("localStorage");
const releaseManagerSessionStorage = () => browserStorage("sessionStorage");
export const getReleaseManagerControlApiUrl = () =>
uniqueUrls([
releaseManagerSessionStorage()?.getItem(RELEASE_MANAGER_LAST_WORKING_CONTROL_API_STORAGE_KEY),
releaseManagerLocalStorage()?.getItem(RELEASE_MANAGER_CONTROL_API_STORAGE_KEY),
RELEASE_MANAGER_CONTROL_API_URL,
API_URL,
])[0] || API_URL;
export const releaseManagerControlApiCandidates = () =>
uniqueUrls([
releaseManagerSessionStorage()?.getItem(RELEASE_MANAGER_LAST_WORKING_CONTROL_API_STORAGE_KEY),
releaseManagerLocalStorage()?.getItem(RELEASE_MANAGER_CONTROL_API_STORAGE_KEY),
RELEASE_MANAGER_CONTROL_API_URL,
...RELEASE_MANAGER_CONTROL_API_FALLBACK_URLS,
API_URL,
]);
export const setReleaseManagerControlApiUrl = (url) => {
const normalized = normalizeApiUrl(url);
if (!normalized) {
return "";
}
releaseManagerLocalStorage()?.setItem(RELEASE_MANAGER_CONTROL_API_STORAGE_KEY, normalized);
releaseManagerSessionStorage()?.setItem(RELEASE_MANAGER_LAST_WORKING_CONTROL_API_STORAGE_KEY, normalized);
return normalized;
};
export const clearReleaseManagerControlApiUrl = () => {
releaseManagerLocalStorage()?.removeItem(RELEASE_MANAGER_CONTROL_API_STORAGE_KEY);
releaseManagerSessionStorage()?.removeItem(RELEASE_MANAGER_LAST_WORKING_CONTROL_API_STORAGE_KEY);
};
const rememberWorkingControlApiUrl = (url) => {
const normalized = normalizeApiUrl(url);
if (normalized) {
releaseManagerSessionStorage()?.setItem(RELEASE_MANAGER_LAST_WORKING_CONTROL_API_STORAGE_KEY, normalized);
}
};
const shouldTryNextControlApi = (error, method) => {
const status = Number(error?.response?.status || 0);
return (
status === 0 ||
(method === "GET" && status === 404) ||
status === 405 ||
status === 501 ||
error?.code === "ERR_NETWORK"
);
};
const buildHeaders = () => {
const headers = {};
const token = releaseManagerLocalStorage()?.getItem("token");
if (token) {
headers.Authorization = `Bearer ${token}`;
}
const isSubuser = releaseManagerLocalStorage()?.getItem("is_subuser") === "true";
const selectedCustomerNumber = getSelectedCustomerNumber();
if (isSubuser && selectedCustomerNumber) {
headers["X-Customer-Number"] = selectedCustomerNumber;
}
return headers;
};
export const requestReleaseManager = (url, method, data = {}) => {
const candidates = releaseManagerControlApiCandidates();
const headers = buildHeaders();
return enqueueRequest(
async () => {
let lastError = null;
for (const baseUrl of candidates) {
try {
const response = await axios({
method,
url: `${baseUrl}${url}`,
...(method === "GET" ? { params: data } : { data }),
__skipRequestQueue: true,
__skipReleaseApiRewrite: true,
headers,
});
rememberWorkingControlApiUrl(baseUrl);
return response;
} catch (error) {
lastError = error;
if (!shouldTryNextControlApi(error, method)) {
throw error;
}
}
}
throw lastError;
},
{
method,
url: `${candidates[0] || API_URL}${url}`,
requestData: {
params: method === "GET" ? data : null,
data: method === "GET" ? null : data,
headers,
},
shouldRetry: () => false,
}
);
};
export const getReleaseSummary = () => requestReleaseManager("/superuser/releases", "GET", {});
export const getReleaseConfig = () => requestReleaseManager("/superuser/releases/config", "GET", {});
export const updateReleaseConfig = (payload) => requestReleaseManager("/superuser/releases/config", "POST", payload);
export const listReleaseChannels = () => requestReleaseManager("/superuser/releases/channels", "GET", {});
export const createReleaseChannel = (payload) => requestReleaseManager("/superuser/releases/channels", "POST", payload);
export const updateReleaseChannel = (id, payload) =>
requestReleaseManager(`/superuser/releases/channels/${id}`, "PATCH", payload);
export const rollbackReleaseChannel = (id) =>
requestReleaseManager(`/superuser/releases/channels/${id}/rollback`, "POST", {});
export const setReleaseChannelBundle = (id, payload) =>
requestReleaseManager(`/superuser/releases/channels/${id}/bundle`, "POST", payload);
export const listReleaseAssignments = () => requestReleaseManager("/superuser/releases/assignments", "GET", {});
export const searchReleaseAssignmentSubjects = (params = {}) =>
requestReleaseManager("/superuser/releases/assignment-subjects", "GET", params);
export const createReleaseAssignment = (payload) =>
requestReleaseManager("/superuser/releases/assignments", "POST", payload);
export const deleteReleaseAssignment = (id) =>
requestReleaseManager(`/superuser/releases/assignments/${id}`, "DELETE", {});
export const listReleaseDeploymentTargets = () => requestReleaseManager("/superuser/releases/targets", "GET", {});
export const saveReleaseDeploymentTarget = (payload) =>
requestReleaseManager("/superuser/releases/targets", "POST", payload);
export const deleteReleaseDeploymentTarget = (id) =>
requestReleaseManager(`/superuser/releases/targets/${id}`, "DELETE", {});
export const listReleaseServiceSets = () => requestReleaseManager("/superuser/releases/service-sets", "GET", {});
export const createReleaseServiceSet = (payload) =>
requestReleaseManager("/superuser/releases/service-sets", "POST", payload);
export const deleteReleaseServiceSet = (id) =>
requestReleaseManager(`/superuser/releases/service-sets/${id}`, "DELETE", {});
export const completeReleaseServiceSetIsolatedDataServices = (id, payload = {}) =>
requestReleaseManager(`/superuser/releases/service-sets/${id}/isolated-data-services`, "POST", payload);
export const listReleaseBundles = (limit = 50) =>
requestReleaseManager("/superuser/releases/bundles", "GET", { limit });
export const createReleaseBundle = (payload) =>
requestReleaseManager("/superuser/releases/bundles", "POST", payload);
export const deployReleaseBundle = (id) =>
requestReleaseManager(`/superuser/releases/bundles/${id}/deploy`, "POST", {});
export const promoteReleaseBundle = (id) =>
requestReleaseManager(`/superuser/releases/bundles/${id}/promote`, "POST", {});
export const listReleaseGithubRepositories = (params = {}) =>
requestReleaseManager("/superuser/releases/github/repositories", "GET", params);
export const listReleaseGithubBranches = (repository) =>
requestReleaseManager("/superuser/releases/github/branches", "GET", { repository });
export const listReleaseGithubCommits = ({ repository, branch, query = "", commit_sha = "" } = {}) =>
requestReleaseManager("/superuser/releases/github/commits", "GET", { repository, branch, query, commit_sha });
export const testReleaseGithubAccess = (payload) =>
requestReleaseManager("/superuser/releases/github/test", "POST", payload);
export const listReleaseDeployments = (limit = 50) =>
requestReleaseManager("/superuser/releases/deployments", "GET", { limit });
export const startReleaseDeployment = (payload) =>
requestReleaseManager("/superuser/releases/deployments", "POST", payload);
export const promoteReleaseDeployment = (id) =>
requestReleaseManager(`/superuser/releases/deployments/${id}/promote`, "POST", {});
export const runReleaseIssueAction = (payload) =>
requestReleaseManager("/superuser/releases/issues/actions", "POST", payload);
export const setReleaseReplayTarget = (payload) =>
requestReleaseManager("/superuser/releases/replay-targets", "POST", payload);
export const searchReleaseTimeline = (filters = {}) =>
requestReleaseManager("/superuser/releases/timeline", "GET", filters);
export const listReleaseTimelineSessions = (filters = {}) =>
requestReleaseManager("/superuser/releases/timeline/sessions", "GET", filters);
export const getReleaseTimelineSession = (traceId) =>
requestReleaseManager(`/superuser/releases/timeline/sessions/${encodeURIComponent(traceId)}`, "GET", {});