chore: Refactor API (#36412)
Fixes #36481 /ok-to-test tags="@tag.All" <!-- This is an auto-generated comment: Cypress test results --> > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: <https://github.com/appsmithorg/appsmith/actions/runs/11028542060> > Commit: ed537d3958a3eba4502cbc32daf60c4cd814002d > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=11028542060&attempt=1" target="_blank">Cypress dashboard</a>. > Tags: `@tag.All` > Spec: > <hr>Wed, 25 Sep 2024 08:56:31 UTC <!-- end of auto-generated comment: Cypress test results --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit - **New Features** - Enhanced API interaction with new default configurations for requests. - Improved error handling with a centralized interceptor for managing various API response errors. - Introduced access control for specific API endpoints through blocked and enabled route management. - Streamlined environment-specific configurations for better maintainability. - Added functionalities for managing application themes, including fetching, updating, and deleting themes. - Introduced new API functions for retrieving consolidated page load data for viewing and editing. - Centralized access point for API services related to theming and consolidated page load data. - New modular structure for API request and response interceptors to improve organization and maintainability. - **Tests** - Added unit tests for both API response and request interceptors to ensure correct functionality and error handling. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Pawan Kumar <pawankumar@Pawans-MacBook-Pro-2.local>
This commit is contained in:
parent
1210104575
commit
2f2f5a6bf4
4
app/client/packages/utils/src/compose/compose.ts
Normal file
4
app/client/packages/utils/src/compose/compose.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export const compose =
|
||||
<T>(...fns: Array<(arg: T) => T>) =>
|
||||
(x: T) =>
|
||||
fns.reduce((acc, fn) => fn(acc), x);
|
||||
1
app/client/packages/utils/src/compose/index.ts
Normal file
1
app/client/packages/utils/src/compose/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { compose } from "./compose";
|
||||
|
|
@ -1 +1,2 @@
|
|||
export * from "./object";
|
||||
export * from "./compose";
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
import type { AxiosInstance, AxiosRequestConfig } from "axios";
|
||||
import axios from "axios";
|
||||
import type { AxiosInstance, AxiosRequestConfig } from "axios";
|
||||
import {
|
||||
apiRequestInterceptor,
|
||||
apiFailureResponseInterceptor,
|
||||
apiSuccessResponseInterceptor,
|
||||
} from "./interceptors";
|
||||
import { REQUEST_TIMEOUT_MS } from "ee/constants/ApiConstants";
|
||||
import { convertObjectToQueryParams } from "utils/URLUtils";
|
||||
import {
|
||||
apiFailureResponseInterceptor,
|
||||
apiRequestInterceptor,
|
||||
apiSuccessResponseInterceptor,
|
||||
blockedApiRoutesForAirgapInterceptor,
|
||||
} from "ee/api/ApiUtils";
|
||||
|
||||
//TODO(abhinav): Refactor this to make more composable.
|
||||
export const apiRequestConfig = {
|
||||
baseURL: "/api/",
|
||||
timeout: REQUEST_TIMEOUT_MS,
|
||||
|
|
@ -21,16 +19,7 @@ export const apiRequestConfig = {
|
|||
|
||||
const axiosInstance: AxiosInstance = axios.create();
|
||||
|
||||
const requestInterceptors = [
|
||||
blockedApiRoutesForAirgapInterceptor,
|
||||
apiRequestInterceptor,
|
||||
];
|
||||
|
||||
requestInterceptors.forEach((interceptor) => {
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
axiosInstance.interceptors.request.use(interceptor as any);
|
||||
});
|
||||
axiosInstance.interceptors.request.use(apiRequestInterceptor);
|
||||
|
||||
axiosInstance.interceptors.response.use(
|
||||
apiSuccessResponseInterceptor,
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
import Api from "./Api";
|
||||
import type { AxiosPromise } from "axios";
|
||||
import type { ApiResponse } from "api/ApiResponses";
|
||||
|
||||
import type { InitConsolidatedApi } from "sagas/InitSagas";
|
||||
|
||||
class ConsolidatedPageLoadApi extends Api {
|
||||
static url = "v1/consolidated-api";
|
||||
static consolidatedApiViewUrl = `${ConsolidatedPageLoadApi.url}/view`;
|
||||
static consolidatedApiEditUrl = `${ConsolidatedPageLoadApi.url}/edit`;
|
||||
|
||||
static async getConsolidatedPageLoadDataView(params: {
|
||||
applicationId?: string;
|
||||
defaultPageId?: string;
|
||||
}): Promise<AxiosPromise<ApiResponse<InitConsolidatedApi>>> {
|
||||
return Api.get(ConsolidatedPageLoadApi.consolidatedApiViewUrl, params);
|
||||
}
|
||||
static async getConsolidatedPageLoadDataEdit(params: {
|
||||
applicationId?: string;
|
||||
defaultPageId?: string;
|
||||
}): Promise<AxiosPromise<ApiResponse<InitConsolidatedApi>>> {
|
||||
return Api.get(ConsolidatedPageLoadApi.consolidatedApiEditUrl, params);
|
||||
}
|
||||
}
|
||||
|
||||
export default ConsolidatedPageLoadApi;
|
||||
225
app/client/src/api/__tests__/apiFailureResponseInterceptors.ts
Normal file
225
app/client/src/api/__tests__/apiFailureResponseInterceptors.ts
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
import axios from "axios";
|
||||
import type { AxiosError } from "axios";
|
||||
|
||||
import {
|
||||
apiFailureResponseInterceptor,
|
||||
apiSuccessResponseInterceptor,
|
||||
} from "api/interceptors";
|
||||
import type { ApiResponse } from "api/types";
|
||||
import {
|
||||
createMessage,
|
||||
ERROR_0,
|
||||
ERROR_413,
|
||||
ERROR_500,
|
||||
SERVER_API_TIMEOUT_ERROR,
|
||||
} from "ee/constants/messages";
|
||||
import { ERROR_CODES } from "ee/constants/ApiConstants";
|
||||
import { UserCancelledActionExecutionError } from "sagas/ActionExecution/errorUtils";
|
||||
|
||||
describe("Api success response interceptors", () => {
|
||||
beforeAll(() => {
|
||||
axios.interceptors.response.use(
|
||||
apiSuccessResponseInterceptor,
|
||||
apiFailureResponseInterceptor,
|
||||
);
|
||||
});
|
||||
|
||||
it("checks 413 error", async () => {
|
||||
axios.defaults.adapter = async () => {
|
||||
return Promise.reject({
|
||||
response: {
|
||||
status: 413,
|
||||
},
|
||||
} as AxiosError);
|
||||
};
|
||||
|
||||
try {
|
||||
await axios.get("https://example.com");
|
||||
} catch (error) {
|
||||
expect((error as AxiosError<ApiResponse>).response?.status).toBe(413);
|
||||
expect((error as AxiosError<ApiResponse>).message).toBe(
|
||||
createMessage(ERROR_413, 100),
|
||||
);
|
||||
expect(
|
||||
(error as AxiosError<ApiResponse> & { statusCode?: string }).statusCode,
|
||||
).toBe("AE-APP-4013");
|
||||
}
|
||||
|
||||
axios.defaults.adapter = undefined;
|
||||
});
|
||||
|
||||
it("checks the response message when request is made when user is offline", async () => {
|
||||
const onlineGetter: jest.SpyInstance = jest.spyOn(
|
||||
window.navigator,
|
||||
"onLine",
|
||||
"get",
|
||||
);
|
||||
|
||||
onlineGetter.mockReturnValue(false);
|
||||
axios.defaults.adapter = async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
reject({
|
||||
message: "Network Error",
|
||||
} as AxiosError);
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
await axios.get("https://example.com");
|
||||
} catch (error) {
|
||||
expect((error as AxiosError<ApiResponse>).message).toBe(
|
||||
createMessage(ERROR_0),
|
||||
);
|
||||
}
|
||||
|
||||
onlineGetter.mockRestore();
|
||||
axios.defaults.adapter = undefined;
|
||||
});
|
||||
|
||||
it("checks if it throws UserCancelledActionExecutionError user cancels the request ", async () => {
|
||||
const cancelToken = axios.CancelToken.source();
|
||||
|
||||
axios.defaults.adapter = async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
cancelToken.cancel("User cancelled the request");
|
||||
|
||||
reject({
|
||||
message: "User cancelled the request",
|
||||
} as AxiosError);
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
await axios.get("https://example.com", {
|
||||
cancelToken: cancelToken.token,
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(UserCancelledActionExecutionError);
|
||||
}
|
||||
|
||||
axios.defaults.adapter = undefined;
|
||||
});
|
||||
|
||||
it("checks the response message when request fails for exeuction action urls", async () => {
|
||||
axios.defaults.adapter = async () => {
|
||||
return Promise.reject({
|
||||
response: {
|
||||
status: 500,
|
||||
statusText: "Internal Server Error",
|
||||
headers: {
|
||||
"content-length": 1,
|
||||
},
|
||||
config: {
|
||||
headers: {
|
||||
timer: "1000",
|
||||
},
|
||||
},
|
||||
},
|
||||
config: {
|
||||
url: "/v1/actions/execute",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const url = "/v1/actions/execute";
|
||||
const response = await axios.get(url);
|
||||
|
||||
expect(response).toHaveProperty("clientMeta");
|
||||
|
||||
axios.defaults.adapter = undefined;
|
||||
});
|
||||
|
||||
it("checks the error response in case of timeout", async () => {
|
||||
axios.defaults.adapter = async () => {
|
||||
return Promise.reject({
|
||||
code: "ECONNABORTED",
|
||||
message: "timeout of 1000ms exceeded",
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
await axios.get("https://example.com");
|
||||
} catch (error) {
|
||||
expect((error as AxiosError<ApiResponse>).message).toBe(
|
||||
createMessage(SERVER_API_TIMEOUT_ERROR),
|
||||
);
|
||||
expect((error as AxiosError<ApiResponse>).code).toBe(
|
||||
ERROR_CODES.REQUEST_TIMEOUT,
|
||||
);
|
||||
}
|
||||
|
||||
axios.defaults.adapter = undefined;
|
||||
});
|
||||
|
||||
it("checks the error response in case of server error", async () => {
|
||||
axios.defaults.adapter = async () => {
|
||||
return Promise.reject({
|
||||
response: {
|
||||
status: 502,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
await axios.get("https://example.com");
|
||||
} catch (error) {
|
||||
expect((error as AxiosError<ApiResponse>).message).toBe(
|
||||
createMessage(ERROR_500),
|
||||
);
|
||||
expect((error as AxiosError<ApiResponse>).code).toBe(
|
||||
ERROR_CODES.SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
axios.defaults.adapter = undefined;
|
||||
});
|
||||
|
||||
it("checks error response in case of unauthorized error", async () => {
|
||||
axios.defaults.adapter = async () => {
|
||||
return Promise.reject({
|
||||
response: {
|
||||
status: 401,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
await axios.get("https://example.com");
|
||||
} catch (error) {
|
||||
expect((error as AxiosError<ApiResponse>).message).toBe(
|
||||
"Unauthorized. Redirecting to login page...",
|
||||
);
|
||||
expect((error as AxiosError<ApiResponse>).code).toBe(
|
||||
ERROR_CODES.REQUEST_NOT_AUTHORISED,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("checks error response in case of not found error", async () => {
|
||||
axios.defaults.adapter = async () => {
|
||||
return Promise.reject({
|
||||
response: {
|
||||
data: {
|
||||
responseMeta: {
|
||||
status: 404,
|
||||
error: {
|
||||
code: "AE-ACL-4004",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
await axios.get("https://example.com");
|
||||
} catch (error) {
|
||||
expect((error as AxiosError<ApiResponse>).message).toBe(
|
||||
"Resource Not Found",
|
||||
);
|
||||
expect((error as AxiosError<ApiResponse>).code).toBe(
|
||||
ERROR_CODES.PAGE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
121
app/client/src/api/__tests__/apiRequestInterceptors.ts
Normal file
121
app/client/src/api/__tests__/apiRequestInterceptors.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import axios from "axios";
|
||||
import {
|
||||
addGitBranchHeader,
|
||||
blockAirgappedRoutes,
|
||||
addRequestedByHeader,
|
||||
addEnvironmentHeader,
|
||||
increaseGitApiTimeout,
|
||||
addAnonymousUserIdHeader,
|
||||
addPerformanceMonitoringHeaders,
|
||||
} from "api/interceptors/request";
|
||||
|
||||
describe("Api request interceptors", () => {
|
||||
beforeAll(() => {
|
||||
axios.defaults.adapter = async (config) => {
|
||||
return new Promise((resolve) => {
|
||||
resolve({
|
||||
data: "Test data",
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: {
|
||||
"content-length": 123,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
config,
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
it("checks if the request config has timer in the headers", async () => {
|
||||
const url = "v1/actions/execute";
|
||||
const identifier = axios.interceptors.request.use(
|
||||
addPerformanceMonitoringHeaders,
|
||||
);
|
||||
const response = await axios.get(url);
|
||||
|
||||
expect(response.config.headers).toHaveProperty("timer");
|
||||
|
||||
axios.interceptors.request.eject(identifier);
|
||||
});
|
||||
|
||||
it("checks if the request config has anonymousUserId in the headers", async () => {
|
||||
const url = "v1/actions/execute";
|
||||
const identifier = axios.interceptors.request.use((config) =>
|
||||
addAnonymousUserIdHeader(config, {
|
||||
segmentEnabled: true,
|
||||
anonymousId: "anonymousUserId",
|
||||
}),
|
||||
);
|
||||
const response = await axios.get(url);
|
||||
|
||||
expect(response.config.headers).toHaveProperty("x-anonymous-user-id");
|
||||
expect(response.config.headers["x-anonymous-user-id"]).toBe(
|
||||
"anonymousUserId",
|
||||
);
|
||||
|
||||
axios.interceptors.request.eject(identifier);
|
||||
});
|
||||
|
||||
it("checks if the request config has csrfToken in the headers", async () => {
|
||||
const url = "v1/actions/execute";
|
||||
const identifier = axios.interceptors.request.use(addRequestedByHeader);
|
||||
const response = await axios.post(url);
|
||||
|
||||
expect(response.config.headers).toHaveProperty("X-Requested-By");
|
||||
expect(response.config.headers["X-Requested-By"]).toBe("Appsmith");
|
||||
|
||||
axios.interceptors.request.eject(identifier);
|
||||
});
|
||||
|
||||
it("checks if the request config has gitBranch in the headers", async () => {
|
||||
const url = "v1/";
|
||||
const identifier = axios.interceptors.request.use((config) => {
|
||||
return addGitBranchHeader(config, { branch: "master" });
|
||||
});
|
||||
const response = await axios.get(url);
|
||||
|
||||
expect(response.config.headers).toHaveProperty("branchName");
|
||||
expect(response.config.headers["branchName"]).toBe("master");
|
||||
|
||||
axios.interceptors.request.eject(identifier);
|
||||
});
|
||||
|
||||
it("checks if the request config has environmentId in the headers", async () => {
|
||||
const url = "v1/saas";
|
||||
const identifier = axios.interceptors.request.use((config) => {
|
||||
return addEnvironmentHeader(config, { env: "default" });
|
||||
});
|
||||
|
||||
const response = await axios.get(url);
|
||||
|
||||
expect(response.config.headers).toHaveProperty("X-Appsmith-EnvironmentId");
|
||||
expect(response.config.headers["X-Appsmith-EnvironmentId"]).toBe("default");
|
||||
|
||||
axios.interceptors.request.eject(identifier);
|
||||
});
|
||||
|
||||
it("checks if request is 200 ok when isAirgapped is true", async () => {
|
||||
const url = "v1/saas";
|
||||
const identifier = axios.interceptors.request.use((config) => {
|
||||
return blockAirgappedRoutes(config, { isAirgapped: true });
|
||||
});
|
||||
const response = await axios.get(url);
|
||||
|
||||
expect(response.data).toBeNull();
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.statusText).toBe("OK");
|
||||
|
||||
axios.interceptors.request.eject(identifier);
|
||||
});
|
||||
|
||||
it("checks if the request config has a timeout of 120s", async () => {
|
||||
const url = "v1/git/";
|
||||
const identifier = axios.interceptors.request.use(increaseGitApiTimeout);
|
||||
const response = await axios.get(url);
|
||||
|
||||
expect(response.config.timeout).toBe(120000);
|
||||
|
||||
axios.interceptors.request.eject(identifier);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import axios from "axios";
|
||||
import { apiSuccessResponseInterceptor } from "api/interceptors";
|
||||
|
||||
describe("Api success response interceptors", () => {
|
||||
beforeAll(() => {
|
||||
axios.interceptors.response.use(apiSuccessResponseInterceptor);
|
||||
axios.defaults.adapter = async (config) => {
|
||||
return new Promise((resolve) => {
|
||||
resolve({
|
||||
data: {
|
||||
data: "Test data",
|
||||
},
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: {
|
||||
"content-length": 123,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
config,
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
it("checks response for non-action-execution url", async () => {
|
||||
const url = "/v1/sass";
|
||||
const response = await axios.get(url);
|
||||
|
||||
expect(response.data).toBe("Test data");
|
||||
});
|
||||
|
||||
it("checks response for action-execution url", async () => {
|
||||
const url = "/v1/actions/execute";
|
||||
const response = await axios.get(url);
|
||||
|
||||
expect(response).toHaveProperty("data");
|
||||
expect(response.data).toBe("Test data");
|
||||
expect(response).toHaveProperty("clientMeta");
|
||||
});
|
||||
});
|
||||
31
app/client/src/api/core/api.ts
Normal file
31
app/client/src/api/core/api.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import type { AxiosResponseData } from "api/types";
|
||||
|
||||
import { apiFactory } from "./factory";
|
||||
|
||||
const apiInstance = apiFactory();
|
||||
|
||||
export async function get<T>(...args: Parameters<typeof apiInstance.get>) {
|
||||
// Note: we are passing AxiosResponseData as the second type argument to set the default type of the response data.The reason
|
||||
// is we modify the response data in the responseSuccessInterceptor to return `.data` property from the axios response so that we can
|
||||
// just `response.data` instead of `response.data.data`. So we have to make sure that the response data's type matches what we do in the interceptor.
|
||||
return apiInstance.get<T, AxiosResponseData<T>>(...args);
|
||||
}
|
||||
|
||||
export async function post<T>(...args: Parameters<typeof apiInstance.post>) {
|
||||
return apiInstance.post<T, AxiosResponseData<T>>(...args);
|
||||
}
|
||||
|
||||
export async function put<T>(...args: Parameters<typeof apiInstance.put>) {
|
||||
return apiInstance.put<T, AxiosResponseData<T>>(...args);
|
||||
}
|
||||
|
||||
// Note: _delete is used instead of delete because delete is a reserved keyword in JavaScript
|
||||
async function _delete<T>(...args: Parameters<typeof apiInstance.delete>) {
|
||||
return apiInstance.delete<T, AxiosResponseData<T>>(...args);
|
||||
}
|
||||
|
||||
export { _delete as delete };
|
||||
|
||||
export async function patch<T>(...args: Parameters<typeof apiInstance.patch>) {
|
||||
return apiInstance.patch<T, AxiosResponseData<T>>(...args);
|
||||
}
|
||||
17
app/client/src/api/core/factory.ts
Normal file
17
app/client/src/api/core/factory.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import axios from "axios";
|
||||
import { DEFAULT_AXIOS_CONFIG } from "ee/constants/ApiConstants";
|
||||
import { apiRequestInterceptor } from "api/interceptors/request/apiRequestInterceptor";
|
||||
import { apiSuccessResponseInterceptor } from "api/interceptors/response/apiSuccessResponseInterceptor";
|
||||
import { apiFailureResponseInterceptor } from "api/interceptors/response/apiFailureResponseInterceptor";
|
||||
|
||||
export function apiFactory() {
|
||||
const axiosInstance = axios.create(DEFAULT_AXIOS_CONFIG);
|
||||
|
||||
axiosInstance.interceptors.request.use(apiRequestInterceptor);
|
||||
axiosInstance.interceptors.response.use(
|
||||
apiSuccessResponseInterceptor,
|
||||
apiFailureResponseInterceptor,
|
||||
);
|
||||
|
||||
return axiosInstance;
|
||||
}
|
||||
2
app/client/src/api/core/index.ts
Normal file
2
app/client/src/api/core/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * as api from "./api";
|
||||
export { apiFactory } from "./factory";
|
||||
12
app/client/src/api/helpers/addExecutionMetaProperties.ts
Normal file
12
app/client/src/api/helpers/addExecutionMetaProperties.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import type { AxiosResponse } from "axios";
|
||||
|
||||
export const addExecutionMetaProperties = (response: AxiosResponse) => {
|
||||
const clientMeta = {
|
||||
size: response.headers["content-length"],
|
||||
duration: Number(
|
||||
performance.now() - response.config.headers.timer,
|
||||
).toFixed(),
|
||||
};
|
||||
|
||||
return { ...response.data, clientMeta };
|
||||
};
|
||||
3
app/client/src/api/helpers/index.ts
Normal file
3
app/client/src/api/helpers/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { is404orAuthPath } from "./is404orAuthPath";
|
||||
export { validateJsonResponseMeta } from "./validateJsonResponseMeta";
|
||||
export { addExecutionMetaProperties } from "./addExecutionMetaProperties";
|
||||
5
app/client/src/api/helpers/is404orAuthPath.ts
Normal file
5
app/client/src/api/helpers/is404orAuthPath.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export const is404orAuthPath = () => {
|
||||
const pathName = window.location.pathname;
|
||||
|
||||
return /^\/404/.test(pathName) || /^\/user\/\w+/.test(pathName);
|
||||
};
|
||||
14
app/client/src/api/helpers/validateJsonResponseMeta.ts
Normal file
14
app/client/src/api/helpers/validateJsonResponseMeta.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import * as Sentry from "@sentry/react";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import { CONTENT_TYPE_HEADER_KEY } from "constants/ApiEditorConstants/CommonApiConstants";
|
||||
|
||||
export const validateJsonResponseMeta = (response: AxiosResponse) => {
|
||||
if (
|
||||
response.headers[CONTENT_TYPE_HEADER_KEY] === "application/json" &&
|
||||
!response.data.responseMeta
|
||||
) {
|
||||
Sentry.captureException(new Error("Api responded without response meta"), {
|
||||
contexts: { response: response.data },
|
||||
});
|
||||
}
|
||||
};
|
||||
1
app/client/src/api/index.ts
Normal file
1
app/client/src/api/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./services";
|
||||
2
app/client/src/api/interceptors/index.ts
Normal file
2
app/client/src/api/interceptors/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./request";
|
||||
export * from "./response";
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import type { InternalAxiosRequestConfig } from "axios";
|
||||
|
||||
export const addAnonymousUserIdHeader = (
|
||||
config: InternalAxiosRequestConfig,
|
||||
options: { anonymousId?: string; segmentEnabled?: boolean },
|
||||
) => {
|
||||
const { anonymousId, segmentEnabled } = options;
|
||||
|
||||
config.headers = config.headers || {};
|
||||
|
||||
if (segmentEnabled && anonymousId) {
|
||||
config.headers["x-anonymous-user-id"] = anonymousId;
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import type { InternalAxiosRequestConfig } from "axios";
|
||||
import { ENV_ENABLED_ROUTES_REGEX } from "ee/constants/ApiConstants";
|
||||
|
||||
export const addEnvironmentHeader = (
|
||||
config: InternalAxiosRequestConfig,
|
||||
options: { env: string },
|
||||
) => {
|
||||
const { env } = options;
|
||||
|
||||
config.headers = config.headers || {};
|
||||
|
||||
if (ENV_ENABLED_ROUTES_REGEX.test(config.url?.split("?")[0] || "")) {
|
||||
if (env) {
|
||||
config.headers["X-Appsmith-EnvironmentId"] = env;
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import type { InternalAxiosRequestConfig } from "axios";
|
||||
|
||||
export const addGitBranchHeader = (
|
||||
config: InternalAxiosRequestConfig,
|
||||
options: { branch?: string },
|
||||
) => {
|
||||
const { branch } = options;
|
||||
|
||||
config.headers = config.headers || {};
|
||||
|
||||
if (branch) {
|
||||
config.headers.branchName = branch;
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import type { InternalAxiosRequestConfig } from "axios";
|
||||
|
||||
export const addPerformanceMonitoringHeaders = (
|
||||
config: InternalAxiosRequestConfig,
|
||||
) => {
|
||||
config.headers = config.headers || {};
|
||||
config.headers["timer"] = performance.now();
|
||||
|
||||
return config;
|
||||
};
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import type { InternalAxiosRequestConfig } from "axios";
|
||||
|
||||
export const addRequestedByHeader = (config: InternalAxiosRequestConfig) => {
|
||||
config.headers = config.headers || {};
|
||||
|
||||
const methodUpper = config.method?.toUpperCase();
|
||||
|
||||
if (methodUpper && methodUpper !== "GET" && methodUpper !== "HEAD") {
|
||||
config.headers["X-Requested-By"] = "Appsmith";
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import store from "store";
|
||||
import { compose } from "@appsmith/utils";
|
||||
import { getAppsmithConfigs } from "ee/configs";
|
||||
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
|
||||
import { isAirgapped } from "ee/utils/airgapHelpers";
|
||||
import type { InternalAxiosRequestConfig } from "axios";
|
||||
|
||||
import getQueryParamsObject from "utils/getQueryParamsObject";
|
||||
import { addRequestedByHeader } from "./addRequestedByHeader";
|
||||
import { increaseGitApiTimeout } from "./increaseGitApiTimeout";
|
||||
import { getCurrentGitBranch } from "selectors/gitSyncSelectors";
|
||||
import { getCurrentEnvironmentId } from "ee/selectors/environmentSelectors";
|
||||
import { addGitBranchHeader as _addGitBranchHeader } from "./addGitBranchHeader";
|
||||
import { addPerformanceMonitoringHeaders } from "./addPerformanceMonitoringHeaders";
|
||||
import { addEnvironmentHeader as _addEnvironmentHeader } from "./addEnvironmentHeader";
|
||||
import { blockAirgappedRoutes as _blockAirgappedRoutes } from "./blockAirgappedRoutes";
|
||||
import { addAnonymousUserIdHeader as _addAnonymousUserIdHeader } from "./addAnonymousUserIdHeader";
|
||||
|
||||
/**
|
||||
* Note: Why can't we use store.getState() or isGapgapped() directly in the interceptor?
|
||||
* The main reason is to easily test the interceptor. When we use store.getState() or isAirgapped() directly in the interceptor,
|
||||
* we need to mock the store or isAirgapped() in the test file and it becomes difficult and messy mocking things just to test the interceptor.
|
||||
*/
|
||||
const blockAirgappedRoutes = (config: InternalAxiosRequestConfig) => {
|
||||
const isAirgappedInstance = isAirgapped();
|
||||
|
||||
return _blockAirgappedRoutes(config, { isAirgapped: isAirgappedInstance });
|
||||
};
|
||||
|
||||
const addGitBranchHeader = (config: InternalAxiosRequestConfig) => {
|
||||
const state = store.getState();
|
||||
const branch = getCurrentGitBranch(state) || getQueryParamsObject().branch;
|
||||
|
||||
return _addGitBranchHeader(config, { branch });
|
||||
};
|
||||
|
||||
const addEnvironmentHeader = (config: InternalAxiosRequestConfig) => {
|
||||
const state = store.getState();
|
||||
const activeEnv = getCurrentEnvironmentId(state);
|
||||
|
||||
return _addEnvironmentHeader(config, { env: activeEnv });
|
||||
};
|
||||
|
||||
const addAnonymousUserIdHeader = (config: InternalAxiosRequestConfig) => {
|
||||
const appsmithConfig = getAppsmithConfigs();
|
||||
const anonymousId = AnalyticsUtil.getAnonymousId();
|
||||
const segmentEnabled = appsmithConfig.segment.enabled;
|
||||
|
||||
return _addAnonymousUserIdHeader(config, { anonymousId, segmentEnabled });
|
||||
};
|
||||
|
||||
export const apiRequestInterceptor = (config: InternalAxiosRequestConfig) => {
|
||||
const interceptorPipeline = compose<InternalAxiosRequestConfig>(
|
||||
blockAirgappedRoutes,
|
||||
addRequestedByHeader,
|
||||
addGitBranchHeader,
|
||||
increaseGitApiTimeout,
|
||||
addEnvironmentHeader,
|
||||
addAnonymousUserIdHeader,
|
||||
addPerformanceMonitoringHeaders,
|
||||
);
|
||||
|
||||
return interceptorPipeline(config);
|
||||
};
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import type { InternalAxiosRequestConfig } from "axios";
|
||||
import { BLOCKED_ROUTES_REGEX } from "ee/constants/ApiConstants";
|
||||
|
||||
const blockAirgappedRoutes = (
|
||||
request: InternalAxiosRequestConfig,
|
||||
options: { isAirgapped: boolean },
|
||||
) => {
|
||||
const { url } = request;
|
||||
const { isAirgapped } = options;
|
||||
|
||||
if (isAirgapped && url && BLOCKED_ROUTES_REGEX.test(url)) {
|
||||
request.adapter = async (config) => {
|
||||
return new Promise((resolve) => {
|
||||
resolve({
|
||||
data: null,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: {},
|
||||
config,
|
||||
request,
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
export { blockAirgappedRoutes };
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import type { InternalAxiosRequestConfig } from "axios";
|
||||
|
||||
export const increaseGitApiTimeout = (config: InternalAxiosRequestConfig) => {
|
||||
if (config.url?.indexOf("/git/") !== -1) {
|
||||
config.timeout = 1000 * 120;
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
8
app/client/src/api/interceptors/request/index.ts
Normal file
8
app/client/src/api/interceptors/request/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export { addGitBranchHeader } from "./addGitBranchHeader";
|
||||
export { blockAirgappedRoutes } from "./blockAirgappedRoutes";
|
||||
export { addRequestedByHeader } from "./addRequestedByHeader";
|
||||
export { addEnvironmentHeader } from "./addEnvironmentHeader";
|
||||
export { apiRequestInterceptor } from "./apiRequestInterceptor";
|
||||
export { increaseGitApiTimeout } from "./increaseGitApiTimeout";
|
||||
export { addAnonymousUserIdHeader } from "./addAnonymousUserIdHeader";
|
||||
export { addPerformanceMonitoringHeaders } from "./addPerformanceMonitoringHeaders";
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import type { AxiosError } from "axios";
|
||||
import type { ApiResponse, ErrorHandler } from "api/types";
|
||||
|
||||
import * as failureHandlers from "./failureHandlers";
|
||||
|
||||
export const apiFailureResponseInterceptor = async (
|
||||
error: AxiosError<ApiResponse>,
|
||||
) => {
|
||||
const handlers: ErrorHandler[] = [
|
||||
failureHandlers.handle413Error,
|
||||
failureHandlers.handleOfflineError,
|
||||
failureHandlers.handleCancelError,
|
||||
failureHandlers.handleExecuteActionError,
|
||||
failureHandlers.handleTimeoutError,
|
||||
failureHandlers.handleServerError,
|
||||
failureHandlers.handleUnauthorizedError,
|
||||
failureHandlers.handleNotFoundError,
|
||||
];
|
||||
|
||||
for (const handler of handlers) {
|
||||
const result = await handler(error);
|
||||
|
||||
if (result !== null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (error?.response?.data.responseMeta) {
|
||||
return Promise.resolve(error.response.data);
|
||||
}
|
||||
|
||||
return Promise.resolve(error);
|
||||
};
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import {
|
||||
validateJsonResponseMeta,
|
||||
addExecutionMetaProperties,
|
||||
} from "api/helpers";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import { EXECUTION_ACTION_REGEX } from "ee/constants/ApiConstants";
|
||||
|
||||
export const apiSuccessResponseInterceptor = (response: AxiosResponse) => {
|
||||
if (response?.config?.url?.match(EXECUTION_ACTION_REGEX)) {
|
||||
return addExecutionMetaProperties(response);
|
||||
}
|
||||
|
||||
validateJsonResponseMeta(response);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import type { AxiosError } from "axios";
|
||||
import {
|
||||
createMessage,
|
||||
ERROR_413,
|
||||
GENERIC_API_EXECUTION_ERROR,
|
||||
} from "ee/constants/messages";
|
||||
|
||||
export const handle413Error = async (error: AxiosError) => {
|
||||
if (error?.response?.status === 413) {
|
||||
return Promise.reject({
|
||||
...error,
|
||||
clientDefinedError: true,
|
||||
statusCode: "AE-APP-4013",
|
||||
message: createMessage(ERROR_413, 100),
|
||||
pluginErrorDetails: {
|
||||
appsmithErrorCode: "AE-APP-4013",
|
||||
appsmithErrorMessage: createMessage(ERROR_413, 100),
|
||||
errorType: "INTERNAL_ERROR", // this value is from the server, hence cannot construct enum type.
|
||||
title: createMessage(GENERIC_API_EXECUTION_ERROR),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import axios from "axios";
|
||||
import type { AxiosError } from "axios";
|
||||
import { UserCancelledActionExecutionError } from "sagas/ActionExecution/errorUtils";
|
||||
|
||||
export async function handleCancelError(error: AxiosError) {
|
||||
if (axios.isCancel(error)) {
|
||||
throw new UserCancelledActionExecutionError();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import type { AxiosError, AxiosResponse } from "axios";
|
||||
import { addExecutionMetaProperties } from "api/helpers";
|
||||
import { EXECUTION_ACTION_REGEX } from "ee/constants/ApiConstants";
|
||||
|
||||
export function handleExecuteActionError(error: AxiosError) {
|
||||
const isExecutionActionURL =
|
||||
error.config && error?.config?.url?.match(EXECUTION_ACTION_REGEX);
|
||||
|
||||
if (isExecutionActionURL) {
|
||||
return addExecutionMetaProperties(error?.response as AxiosResponse);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import type { AxiosError } from "axios";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import type { ApiResponse } from "api/types";
|
||||
|
||||
export const handleMissingResponseMeta = async (
|
||||
error: AxiosError<ApiResponse>,
|
||||
) => {
|
||||
if (error.response?.data && !error.response.data.responseMeta) {
|
||||
Sentry.captureException(new Error("Api responded without response meta"), {
|
||||
contexts: { response: { ...error.response.data } },
|
||||
});
|
||||
|
||||
return Promise.reject(error.response.data);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import {
|
||||
API_STATUS_CODES,
|
||||
ERROR_CODES,
|
||||
SERVER_ERROR_CODES,
|
||||
} from "ee/constants/ApiConstants";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import type { AxiosError } from "axios";
|
||||
import type { ApiResponse } from "api/types";
|
||||
import { is404orAuthPath } from "api/helpers";
|
||||
|
||||
export async function handleNotFoundError(error: AxiosError<ApiResponse>) {
|
||||
if (is404orAuthPath()) return null;
|
||||
|
||||
const errorData =
|
||||
error?.response?.data.responseMeta ?? ({} as ApiResponse["responseMeta"]);
|
||||
|
||||
if (
|
||||
errorData.status === API_STATUS_CODES.RESOURCE_NOT_FOUND &&
|
||||
errorData.error?.code &&
|
||||
(SERVER_ERROR_CODES.RESOURCE_NOT_FOUND.includes(errorData.error?.code) ||
|
||||
SERVER_ERROR_CODES.UNABLE_TO_FIND_PAGE.includes(errorData?.error?.code))
|
||||
) {
|
||||
Sentry.captureException(error);
|
||||
|
||||
return Promise.reject({
|
||||
...error,
|
||||
code: ERROR_CODES.PAGE_NOT_FOUND,
|
||||
message: "Resource Not Found",
|
||||
show: false,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import type { AxiosError } from "axios";
|
||||
import { createMessage, ERROR_0 } from "ee/constants/messages";
|
||||
|
||||
export const handleOfflineError = async (error: AxiosError) => {
|
||||
if (!window.navigator.onLine) {
|
||||
return Promise.reject({
|
||||
...error,
|
||||
message: createMessage(ERROR_0),
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import type { AxiosError } from "axios";
|
||||
import { createMessage, ERROR_500 } from "ee/constants/messages";
|
||||
import { API_STATUS_CODES, ERROR_CODES } from "ee/constants/ApiConstants";
|
||||
|
||||
export const handleServerError = async (error: AxiosError) => {
|
||||
if (error.response?.status === API_STATUS_CODES.SERVER_ERROR) {
|
||||
return Promise.reject({
|
||||
...error,
|
||||
code: ERROR_CODES.SERVER_ERROR,
|
||||
message: createMessage(ERROR_500),
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import type { AxiosError } from "axios";
|
||||
import {
|
||||
ERROR_CODES,
|
||||
TIMEOUT_ERROR_REGEX,
|
||||
AXIOS_CONNECTION_ABORTED_CODE,
|
||||
} from "ee/constants/ApiConstants";
|
||||
import { createMessage, SERVER_API_TIMEOUT_ERROR } from "ee/constants/messages";
|
||||
|
||||
export const handleTimeoutError = async (error: AxiosError) => {
|
||||
if (
|
||||
error.code === AXIOS_CONNECTION_ABORTED_CODE &&
|
||||
error.message?.match(TIMEOUT_ERROR_REGEX)
|
||||
) {
|
||||
return Promise.reject({
|
||||
...error,
|
||||
message: createMessage(SERVER_API_TIMEOUT_ERROR),
|
||||
code: ERROR_CODES.REQUEST_TIMEOUT,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import store from "store";
|
||||
import type { AxiosError } from "axios";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { is404orAuthPath } from "api/helpers";
|
||||
import { logoutUser } from "actions/userActions";
|
||||
import { AUTH_LOGIN_URL } from "constants/routes";
|
||||
import { API_STATUS_CODES, ERROR_CODES } from "ee/constants/ApiConstants";
|
||||
|
||||
export const handleUnauthorizedError = async (error: AxiosError) => {
|
||||
if (is404orAuthPath()) return null;
|
||||
|
||||
if (error.response?.status === API_STATUS_CODES.REQUEST_NOT_AUTHORISED) {
|
||||
const currentUrl = `${window.location.href}`;
|
||||
|
||||
store.dispatch(
|
||||
logoutUser({
|
||||
redirectURL: `${AUTH_LOGIN_URL}?redirectUrl=${encodeURIComponent(
|
||||
currentUrl,
|
||||
)}`,
|
||||
}),
|
||||
);
|
||||
|
||||
Sentry.captureException(error);
|
||||
|
||||
return Promise.reject({
|
||||
...error,
|
||||
code: ERROR_CODES.REQUEST_NOT_AUTHORISED,
|
||||
message: "Unauthorized. Redirecting to login page...",
|
||||
show: false,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
export { handle413Error } from "./handle413Error";
|
||||
export { handleServerError } from "./handleServerError";
|
||||
export { handleCancelError } from "./handleCancelError";
|
||||
export { handleOfflineError } from "./handleOfflineError";
|
||||
export { handleTimeoutError } from "./handleTimeoutError";
|
||||
export { handleNotFoundError } from "./handleNotFoundError";
|
||||
export { handleUnauthorizedError } from "./handleUnauthorizedError";
|
||||
export { handleExecuteActionError } from "./handleExecuteActionError";
|
||||
export { handleMissingResponseMeta } from "./handleMissingResponseMeta";
|
||||
2
app/client/src/api/interceptors/response/index.ts
Normal file
2
app/client/src/api/interceptors/response/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { apiFailureResponseInterceptor } from "./apiFailureResponseInterceptor";
|
||||
export { apiSuccessResponseInterceptor } from "./apiSuccessResponseInterceptor";
|
||||
38
app/client/src/api/services/AppThemingApi/api.ts
Normal file
38
app/client/src/api/services/AppThemingApi/api.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { api } from "api/core";
|
||||
import type { AppTheme } from "entities/AppTheming";
|
||||
|
||||
const baseURL = "/v1";
|
||||
|
||||
export async function fetchThemes(applicationId: string) {
|
||||
const url = `${baseURL}/themes/applications/${applicationId}`;
|
||||
|
||||
return api.get<AppTheme[]>(url);
|
||||
}
|
||||
|
||||
export async function fetchSelected(applicationId: string, mode = "EDIT") {
|
||||
const url = `${baseURL}/themes/applications/${applicationId}/current`;
|
||||
|
||||
return api.get<AppTheme[]>(url, { params: { mode } });
|
||||
}
|
||||
|
||||
export async function updateTheme(applicationId: string, theme: AppTheme) {
|
||||
const url = `${baseURL}/themes/applications/${applicationId}`;
|
||||
const payload = {
|
||||
...theme,
|
||||
new: undefined,
|
||||
};
|
||||
|
||||
return api.put<AppTheme[]>(url, payload);
|
||||
}
|
||||
|
||||
export async function changeTheme(applicationId: string, theme: AppTheme) {
|
||||
const url = `${baseURL}/applications/${applicationId}/themes/${theme.id}`;
|
||||
|
||||
return api.patch<AppTheme[]>(url, theme);
|
||||
}
|
||||
|
||||
export async function deleteTheme(themeId: string) {
|
||||
const url = `${baseURL}/themes/${themeId}`;
|
||||
|
||||
return api.delete<AppTheme[]>(url);
|
||||
}
|
||||
1
app/client/src/api/services/AppThemingApi/index.ts
Normal file
1
app/client/src/api/services/AppThemingApi/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./api";
|
||||
20
app/client/src/api/services/ConsolidatedPageLoadApi/api.ts
Normal file
20
app/client/src/api/services/ConsolidatedPageLoadApi/api.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { api } from "api/core";
|
||||
import type { InitConsolidatedApi } from "sagas/InitSagas";
|
||||
|
||||
const BASE_URL = "v1/consolidated-api";
|
||||
const VIEW_URL = `${BASE_URL}/view`;
|
||||
const EDIT_URL = `${BASE_URL}/edit`;
|
||||
|
||||
export const getConsolidatedPageLoadDataView = async (params: {
|
||||
applicationId?: string;
|
||||
defaultPageId?: string;
|
||||
}) => {
|
||||
return api.get<InitConsolidatedApi>(VIEW_URL, { params });
|
||||
};
|
||||
|
||||
export const getConsolidatedPageLoadDataEdit = async (params: {
|
||||
applicationId?: string;
|
||||
defaultPageId?: string;
|
||||
}) => {
|
||||
return api.get<InitConsolidatedApi>(EDIT_URL, { params });
|
||||
};
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from "./api";
|
||||
2
app/client/src/api/services/index.ts
Normal file
2
app/client/src/api/services/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * as AppThemingApi from "./AppThemingApi";
|
||||
export * as ConsolidatedPageLoadApi from "./ConsolidatedPageLoadApi";
|
||||
24
app/client/src/api/types.ts
Normal file
24
app/client/src/api/types.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import type { AxiosError, AxiosResponse } from "axios";
|
||||
|
||||
export interface ApiResponseError {
|
||||
code: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ApiResponseMeta {
|
||||
status: number;
|
||||
success: boolean;
|
||||
error?: ApiResponseError;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T = unknown> {
|
||||
responseMeta: ApiResponseMeta;
|
||||
data: T;
|
||||
code?: string;
|
||||
}
|
||||
|
||||
export type AxiosResponseData<T> = AxiosResponse<ApiResponse<T>>["data"];
|
||||
|
||||
export type ErrorHandler = (
|
||||
error: AxiosError<ApiResponse>,
|
||||
) => Promise<unknown | null>;
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
import {
|
||||
apiRequestInterceptor,
|
||||
apiSuccessResponseInterceptor,
|
||||
apiFailureResponseInterceptor,
|
||||
axiosConnectionAbortedCode,
|
||||
} from "./ApiUtils";
|
||||
import type { AxiosRequestConfig, AxiosResponse } from "axios";
|
||||
import type { ActionExecutionResponse } from "api/ActionAPI";
|
||||
import {
|
||||
createMessage,
|
||||
ERROR_0,
|
||||
SERVER_API_TIMEOUT_ERROR,
|
||||
} from "ee/constants/messages";
|
||||
import { ERROR_CODES } from "ee/constants/ApiConstants";
|
||||
import * as Sentry from "@sentry/react";
|
||||
|
||||
describe("axios api interceptors", () => {
|
||||
describe("Axios api request interceptor", () => {
|
||||
it("adds timer to the request object", () => {
|
||||
const request: AxiosRequestConfig = {
|
||||
url: "https://app.appsmith.com/v1/api/actions/execute",
|
||||
};
|
||||
const interceptedRequest = apiRequestInterceptor(request);
|
||||
|
||||
expect(interceptedRequest).toHaveProperty("timer");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Axios api response success interceptor", () => {
|
||||
it("transforms an action execution response", () => {
|
||||
const response: AxiosResponse = {
|
||||
data: "Test data",
|
||||
headers: {
|
||||
"content-length": 123,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
config: {
|
||||
url: "https://app.appsmith.com/v1/api/actions/execute",
|
||||
// @ts-expect-error: type mismatch
|
||||
timer: 0,
|
||||
},
|
||||
};
|
||||
|
||||
const interceptedResponse: ActionExecutionResponse =
|
||||
apiSuccessResponseInterceptor(response);
|
||||
|
||||
expect(interceptedResponse).toHaveProperty("clientMeta");
|
||||
expect(interceptedResponse.clientMeta).toHaveProperty("size");
|
||||
expect(interceptedResponse.clientMeta.size).toBe(123);
|
||||
expect(interceptedResponse.clientMeta).toHaveProperty("duration");
|
||||
});
|
||||
|
||||
it("just returns the response data for other requests", () => {
|
||||
const response: AxiosResponse = {
|
||||
data: "Test data",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
config: {
|
||||
url: "https://app.appsmith.com/v1/api/actions",
|
||||
//@ts-expect-error: type mismatch
|
||||
timer: 0,
|
||||
},
|
||||
};
|
||||
|
||||
const interceptedResponse: ActionExecutionResponse =
|
||||
apiSuccessResponseInterceptor(response);
|
||||
|
||||
expect(interceptedResponse).toBe("Test data");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Api response failure interceptor", () => {
|
||||
beforeEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("checks for no internet errors", () => {
|
||||
jest.spyOn(navigator, "onLine", "get").mockReturnValue(false);
|
||||
const interceptedResponse = apiFailureResponseInterceptor({});
|
||||
|
||||
expect(interceptedResponse).rejects.toStrictEqual({
|
||||
message: createMessage(ERROR_0),
|
||||
});
|
||||
});
|
||||
|
||||
it.todo("handles axios cancel gracefully");
|
||||
|
||||
it("handles timeout errors", () => {
|
||||
const error = {
|
||||
code: axiosConnectionAbortedCode,
|
||||
message: "timeout of 10000ms exceeded",
|
||||
};
|
||||
const interceptedResponse = apiFailureResponseInterceptor(error);
|
||||
|
||||
expect(interceptedResponse).rejects.toStrictEqual({
|
||||
message: createMessage(SERVER_API_TIMEOUT_ERROR),
|
||||
code: ERROR_CODES.REQUEST_TIMEOUT,
|
||||
});
|
||||
});
|
||||
|
||||
it("checks for response meta", () => {
|
||||
const sentrySpy = jest.spyOn(Sentry, "captureException");
|
||||
const response: AxiosResponse = {
|
||||
data: "Test data",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
config: {
|
||||
url: "https://app.appsmith.com/v1/api/user",
|
||||
//@ts-expect-error: type mismatch
|
||||
timer: 0,
|
||||
},
|
||||
};
|
||||
|
||||
apiSuccessResponseInterceptor(response);
|
||||
expect(sentrySpy).toHaveBeenCalled();
|
||||
|
||||
const interceptedFailureResponse = apiFailureResponseInterceptor({
|
||||
response,
|
||||
});
|
||||
|
||||
expect(interceptedFailureResponse).rejects.toStrictEqual("Test data");
|
||||
expect(sentrySpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,289 +1,7 @@
|
|||
import {
|
||||
createMessage,
|
||||
ERROR_0,
|
||||
ERROR_413,
|
||||
ERROR_500,
|
||||
GENERIC_API_EXECUTION_ERROR,
|
||||
SERVER_API_TIMEOUT_ERROR,
|
||||
} from "ee/constants/messages";
|
||||
import type { AxiosRequestConfig, AxiosResponse } from "axios";
|
||||
import axios from "axios";
|
||||
import {
|
||||
API_STATUS_CODES,
|
||||
ERROR_CODES,
|
||||
SERVER_ERROR_CODES,
|
||||
} from "ee/constants/ApiConstants";
|
||||
import log from "loglevel";
|
||||
import type { ActionExecutionResponse } from "api/ActionAPI";
|
||||
import store from "store";
|
||||
import { logoutUser } from "actions/userActions";
|
||||
import { AUTH_LOGIN_URL } from "constants/routes";
|
||||
import { getCurrentGitBranch } from "selectors/gitSyncSelectors";
|
||||
import getQueryParamsObject from "utils/getQueryParamsObject";
|
||||
import { UserCancelledActionExecutionError } from "sagas/ActionExecution/errorUtils";
|
||||
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
|
||||
import { getAppsmithConfigs } from "ee/configs";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { CONTENT_TYPE_HEADER_KEY } from "constants/ApiEditorConstants/CommonApiConstants";
|
||||
import { isAirgapped } from "ee/utils/airgapHelpers";
|
||||
import { getCurrentEnvironmentId } from "ee/selectors/environmentSelectors";
|
||||
import { UNUSED_ENV_ID } from "constants/EnvironmentContants";
|
||||
import { ID_EXTRACTION_REGEX } from "ee/constants/routes/appRoutes";
|
||||
|
||||
const executeActionRegex = /actions\/execute/;
|
||||
const timeoutErrorRegex = /timeout of (\d+)ms exceeded/;
|
||||
|
||||
export const axiosConnectionAbortedCode = "ECONNABORTED";
|
||||
const appsmithConfig = getAppsmithConfigs();
|
||||
|
||||
export const DEFAULT_ENV_ID = UNUSED_ENV_ID;
|
||||
|
||||
export const BLOCKED_ROUTES = [
|
||||
"v1/app-templates",
|
||||
"v1/datasources/mocks",
|
||||
"v1/usage-pulse",
|
||||
"v1/applications/releaseItems",
|
||||
"v1/saas",
|
||||
];
|
||||
|
||||
export const BLOCKED_ROUTES_REGEX = new RegExp(
|
||||
`^(${BLOCKED_ROUTES.join("|")})($|/)`,
|
||||
);
|
||||
|
||||
export const ENV_ENABLED_ROUTES = [
|
||||
`v1/datasources/${ID_EXTRACTION_REGEX}/structure`,
|
||||
`/v1/datasources/${ID_EXTRACTION_REGEX}/trigger`,
|
||||
"v1/actions/execute",
|
||||
"v1/saas",
|
||||
];
|
||||
|
||||
export const ENV_ENABLED_ROUTES_REGEX = new RegExp(
|
||||
`^(${ENV_ENABLED_ROUTES.join("|")})($|/)`,
|
||||
);
|
||||
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const makeExecuteActionResponse = (response: any): ActionExecutionResponse => ({
|
||||
...response.data,
|
||||
clientMeta: {
|
||||
size: response.headers["content-length"],
|
||||
duration: Number(performance.now() - response.config.timer).toFixed(),
|
||||
},
|
||||
});
|
||||
|
||||
const is404orAuthPath = () => {
|
||||
const pathName = window.location.pathname;
|
||||
|
||||
return /^\/404/.test(pathName) || /^\/user\/\w+/.test(pathName);
|
||||
};
|
||||
|
||||
export const blockedApiRoutesForAirgapInterceptor = async (
|
||||
config: AxiosRequestConfig,
|
||||
) => {
|
||||
const { url } = config;
|
||||
|
||||
const isAirgappedInstance = isAirgapped();
|
||||
|
||||
if (isAirgappedInstance && url && BLOCKED_ROUTES_REGEX.test(url)) {
|
||||
return Promise.resolve({ data: null, status: 200 });
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
// Request interceptor will add a timer property to the request.
|
||||
// this will be used to calculate the time taken for an action
|
||||
// execution request
|
||||
export const apiRequestInterceptor = (config: AxiosRequestConfig) => {
|
||||
config.headers = config.headers ?? {};
|
||||
|
||||
// Add header for CSRF protection.
|
||||
const methodUpper = config.method?.toUpperCase();
|
||||
|
||||
if (methodUpper && methodUpper !== "GET" && methodUpper !== "HEAD") {
|
||||
config.headers["X-Requested-By"] = "Appsmith";
|
||||
}
|
||||
|
||||
const state = store.getState();
|
||||
const branch = getCurrentGitBranch(state) || getQueryParamsObject().branch;
|
||||
|
||||
if (branch && config.headers) {
|
||||
config.headers.branchName = branch;
|
||||
}
|
||||
|
||||
if (config.url?.indexOf("/git/") !== -1) {
|
||||
config.timeout = 1000 * 120; // increase timeout for git specific APIs
|
||||
}
|
||||
|
||||
if (ENV_ENABLED_ROUTES_REGEX.test(config.url?.split("?")[0] || "")) {
|
||||
// Add header for environment name
|
||||
const activeEnv = getCurrentEnvironmentId(state);
|
||||
|
||||
if (activeEnv && config.headers) {
|
||||
config.headers["X-Appsmith-EnvironmentId"] = activeEnv;
|
||||
}
|
||||
}
|
||||
|
||||
const anonymousId = AnalyticsUtil.getAnonymousId();
|
||||
|
||||
appsmithConfig.segment.enabled &&
|
||||
anonymousId &&
|
||||
(config.headers["x-anonymous-user-id"] = anonymousId);
|
||||
|
||||
return { ...config, timer: performance.now() };
|
||||
};
|
||||
|
||||
// On success of an API, if the api is an action execution,
|
||||
// add the client meta object with size and time taken info
|
||||
// otherwise just return the data
|
||||
export const apiSuccessResponseInterceptor = (
|
||||
response: AxiosResponse,
|
||||
): AxiosResponse["data"] => {
|
||||
if (response.config.url) {
|
||||
if (response.config.url.match(executeActionRegex)) {
|
||||
return makeExecuteActionResponse(response);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
response.headers[CONTENT_TYPE_HEADER_KEY] === "application/json" &&
|
||||
!response.data.responseMeta
|
||||
) {
|
||||
Sentry.captureException(new Error("Api responded without response meta"), {
|
||||
contexts: { response: response.data },
|
||||
});
|
||||
}
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// Handle different api failure scenarios
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const apiFailureResponseInterceptor = async (error: any) => {
|
||||
// this can be extended to other errors we want to catch.
|
||||
// in this case it is 413.
|
||||
if (error && error?.response && error?.response.status === 413) {
|
||||
return Promise.reject({
|
||||
...error,
|
||||
clientDefinedError: true,
|
||||
statusCode: "AE-APP-4013",
|
||||
message: createMessage(ERROR_413, 100),
|
||||
pluginErrorDetails: {
|
||||
appsmithErrorCode: "AE-APP-4013",
|
||||
appsmithErrorMessage: createMessage(ERROR_413, 100),
|
||||
errorType: "INTERNAL_ERROR", // this value is from the server, hence cannot construct enum type.
|
||||
title: createMessage(GENERIC_API_EXECUTION_ERROR),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Return error when there is no internet
|
||||
if (!window.navigator.onLine) {
|
||||
return Promise.reject({
|
||||
...error,
|
||||
message: createMessage(ERROR_0),
|
||||
});
|
||||
}
|
||||
|
||||
// Return if the call was cancelled via cancel token
|
||||
if (axios.isCancel(error)) {
|
||||
throw new UserCancelledActionExecutionError();
|
||||
}
|
||||
|
||||
// Return modified response if action execution failed
|
||||
if (error.config && error.config.url.match(executeActionRegex)) {
|
||||
return makeExecuteActionResponse(error.response);
|
||||
}
|
||||
|
||||
// Return error if any timeout happened in other api calls
|
||||
if (
|
||||
error.code === axiosConnectionAbortedCode &&
|
||||
error.message &&
|
||||
error.message.match(timeoutErrorRegex)
|
||||
) {
|
||||
return Promise.reject({
|
||||
...error,
|
||||
message: createMessage(SERVER_API_TIMEOUT_ERROR),
|
||||
code: ERROR_CODES.REQUEST_TIMEOUT,
|
||||
});
|
||||
}
|
||||
|
||||
if (error.response) {
|
||||
if (error.response.status === API_STATUS_CODES.SERVER_ERROR) {
|
||||
return Promise.reject({
|
||||
...error,
|
||||
code: ERROR_CODES.SERVER_ERROR,
|
||||
message: createMessage(ERROR_500),
|
||||
});
|
||||
}
|
||||
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
if (!is404orAuthPath()) {
|
||||
const currentUrl = `${window.location.href}`;
|
||||
|
||||
if (error.response.status === API_STATUS_CODES.REQUEST_NOT_AUTHORISED) {
|
||||
// Redirect to login and set a redirect url.
|
||||
store.dispatch(
|
||||
logoutUser({
|
||||
redirectURL: `${AUTH_LOGIN_URL}?redirectUrl=${encodeURIComponent(
|
||||
currentUrl,
|
||||
)}`,
|
||||
}),
|
||||
);
|
||||
Sentry.captureException(error);
|
||||
|
||||
return Promise.reject({
|
||||
...error,
|
||||
code: ERROR_CODES.REQUEST_NOT_AUTHORISED,
|
||||
message: "Unauthorized. Redirecting to login page...",
|
||||
show: false,
|
||||
});
|
||||
}
|
||||
|
||||
const errorData = error.response.data.responseMeta ?? {};
|
||||
|
||||
if (
|
||||
errorData.status === API_STATUS_CODES.RESOURCE_NOT_FOUND &&
|
||||
(SERVER_ERROR_CODES.RESOURCE_NOT_FOUND.includes(errorData.error.code) ||
|
||||
SERVER_ERROR_CODES.UNABLE_TO_FIND_PAGE.includes(errorData.error.code))
|
||||
) {
|
||||
Sentry.captureException(error);
|
||||
|
||||
return Promise.reject({
|
||||
...error,
|
||||
code: ERROR_CODES.PAGE_NOT_FOUND,
|
||||
message: "Resource Not Found",
|
||||
show: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (error.response.data.responseMeta) {
|
||||
return Promise.resolve(error.response.data);
|
||||
}
|
||||
|
||||
Sentry.captureException(new Error("Api responded without response meta"), {
|
||||
contexts: { response: error.response.data },
|
||||
});
|
||||
|
||||
return Promise.reject(error.response.data);
|
||||
} else if (error.request) {
|
||||
// The request was made but no response was received
|
||||
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
||||
// http.ClientRequest in node.js
|
||||
log.error(error.request);
|
||||
} else {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
log.error("Error", error.message);
|
||||
}
|
||||
|
||||
log.debug(error.config);
|
||||
|
||||
return Promise.resolve(error);
|
||||
};
|
||||
|
||||
// function to get the default environment
|
||||
export const getDefaultEnvId = () => {
|
||||
return DEFAULT_ENV_ID;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
import type { CreateAxiosDefaults } from "axios";
|
||||
import { ID_EXTRACTION_REGEX } from "constants/routes";
|
||||
import { UNUSED_ENV_ID } from "constants/EnvironmentContants";
|
||||
|
||||
export const REQUEST_TIMEOUT_MS = 20000;
|
||||
export const DEFAULT_ACTION_TIMEOUT = 10000;
|
||||
export const DEFAULT_EXECUTE_ACTION_TIMEOUT_MS = 15000;
|
||||
|
|
@ -5,6 +9,44 @@ export const DEFAULT_TEST_DATA_SOURCE_TIMEOUT_MS = 30000;
|
|||
export const DEFAULT_APPSMITH_AI_QUERY_TIMEOUT_MS = 60000;
|
||||
export const FILE_UPLOAD_TRIGGER_TIMEOUT_MS = 60000;
|
||||
|
||||
export const DEFAULT_AXIOS_CONFIG: CreateAxiosDefaults = {
|
||||
baseURL: "/api/",
|
||||
timeout: REQUEST_TIMEOUT_MS,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
withCredentials: true,
|
||||
};
|
||||
|
||||
export const EXECUTION_ACTION_REGEX = /actions\/execute/;
|
||||
export const TIMEOUT_ERROR_REGEX = /timeout of (\d+)ms exceeded/;
|
||||
export const AXIOS_CONNECTION_ABORTED_CODE = "ECONNABORTED";
|
||||
|
||||
export const DEFAULT_ENV_ID = UNUSED_ENV_ID;
|
||||
|
||||
export const BLOCKED_ROUTES = [
|
||||
"v1/app-templates",
|
||||
"v1/datasources/mocks",
|
||||
"v1/usage-pulse",
|
||||
"v1/applications/releaseItems",
|
||||
"v1/saas",
|
||||
];
|
||||
|
||||
export const BLOCKED_ROUTES_REGEX = new RegExp(
|
||||
`^(${BLOCKED_ROUTES.join("|")})($|/)`,
|
||||
);
|
||||
|
||||
export const ENV_ENABLED_ROUTES = [
|
||||
`v1/datasources/${ID_EXTRACTION_REGEX}/structure`,
|
||||
`/v1/datasources/${ID_EXTRACTION_REGEX}/trigger`,
|
||||
"v1/actions/execute",
|
||||
"v1/saas",
|
||||
];
|
||||
|
||||
export const ENV_ENABLED_ROUTES_REGEX = new RegExp(
|
||||
`^(${ENV_ENABLED_ROUTES.join("|")})($|/)`,
|
||||
);
|
||||
|
||||
export enum API_STATUS_CODES {
|
||||
REQUEST_NOT_AUTHORISED = 401,
|
||||
RESOURCE_NOT_FOUND = 404,
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ import type { LayoutSystemTypes } from "layoutSystems/types";
|
|||
import { getIsAnvilLayout } from "layoutSystems/anvil/integrations/selectors";
|
||||
import { convertToBasePageIdSelector } from "selectors/pageListSelectors";
|
||||
import type { Page } from "entities/Page";
|
||||
import ConsolidatedPageLoadApi from "api/ConsolidatedPageLoadApi";
|
||||
import { ConsolidatedPageLoadApi } from "api";
|
||||
|
||||
export const checkIfMigrationIsNeeded = (
|
||||
fetchPageResponse?: FetchPageResponse,
|
||||
|
|
|
|||
|
|
@ -11,8 +11,15 @@ import {
|
|||
ReduxActionErrorTypes,
|
||||
ReduxActionTypes,
|
||||
} from "ee/constants/ReduxActionConstants";
|
||||
import ThemingApi from "api/AppThemingApi";
|
||||
import { all, takeLatest, put, select, call } from "redux-saga/effects";
|
||||
import { AppThemingApi } from "api";
|
||||
import {
|
||||
all,
|
||||
takeLatest,
|
||||
put,
|
||||
select,
|
||||
call,
|
||||
type SagaReturnType,
|
||||
} from "redux-saga/effects";
|
||||
import { toast } from "@appsmith/ads";
|
||||
import {
|
||||
CHANGE_APP_THEME,
|
||||
|
|
@ -31,8 +38,6 @@ import { getBetaFlag, setBetaFlag, STORAGE_KEYS } from "utils/storage";
|
|||
import type { UpdateWidgetPropertyPayload } from "actions/controlActions";
|
||||
import { batchUpdateMultipleWidgetProperties } from "actions/controlActions";
|
||||
import { getPropertiesToUpdateForReset } from "entities/AppTheming/utils";
|
||||
import type { ApiResponse } from "api/ApiResponses";
|
||||
import type { AppTheme } from "entities/AppTheming";
|
||||
import type { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
|
||||
import {
|
||||
getCurrentApplicationId,
|
||||
|
|
@ -75,11 +80,10 @@ export function* fetchAppThemes(action: ReduxAction<FetchAppThemesAction>) {
|
|||
try {
|
||||
const { applicationId, themes } = action.payload;
|
||||
|
||||
const response: ApiResponse<AppTheme> = yield call(
|
||||
getFromServerWhenNoPrefetchedResult,
|
||||
themes,
|
||||
async () => ThemingApi.fetchThemes(applicationId),
|
||||
);
|
||||
const response: SagaReturnType<typeof AppThemingApi.fetchThemes> =
|
||||
yield call(getFromServerWhenNoPrefetchedResult, themes, async () =>
|
||||
AppThemingApi.fetchThemes(applicationId),
|
||||
);
|
||||
|
||||
yield put({
|
||||
type: ReduxActionTypes.FETCH_APP_THEMES_SUCCESS,
|
||||
|
|
@ -112,11 +116,10 @@ export function* fetchAppSelectedTheme(
|
|||
const applicationVersion = yield select(selectApplicationVersion);
|
||||
|
||||
try {
|
||||
const response: ApiResponse<AppTheme[]> = yield call(
|
||||
getFromServerWhenNoPrefetchedResult,
|
||||
currentTheme,
|
||||
async () => ThemingApi.fetchSelected(applicationId, mode),
|
||||
);
|
||||
const response: SagaReturnType<typeof AppThemingApi.fetchSelected> =
|
||||
yield call(getFromServerWhenNoPrefetchedResult, currentTheme, async () =>
|
||||
AppThemingApi.fetchSelected(applicationId, mode),
|
||||
);
|
||||
|
||||
if (response?.data) {
|
||||
yield put({
|
||||
|
|
@ -161,7 +164,7 @@ export function* updateSelectedTheme(
|
|||
const canvasWidgets: CanvasWidgetsReduxState = yield select(getCanvasWidgets);
|
||||
|
||||
try {
|
||||
yield ThemingApi.updateTheme(applicationId, theme);
|
||||
yield AppThemingApi.updateTheme(applicationId, theme);
|
||||
|
||||
yield put({
|
||||
type: ReduxActionTypes.UPDATE_SELECTED_APP_THEME_SUCCESS,
|
||||
|
|
@ -197,7 +200,7 @@ export function* changeSelectedTheme(
|
|||
const canvasWidgets: CanvasWidgetsReduxState = yield select(getCanvasWidgets);
|
||||
|
||||
try {
|
||||
yield ThemingApi.changeTheme(applicationId, theme);
|
||||
yield AppThemingApi.changeTheme(applicationId, theme);
|
||||
|
||||
yield put({
|
||||
type: ReduxActionTypes.CHANGE_SELECTED_APP_THEME_SUCCESS,
|
||||
|
|
@ -235,7 +238,7 @@ export function* deleteTheme(action: ReduxAction<DeleteAppThemeAction>) {
|
|||
const { name, themeId } = action.payload;
|
||||
|
||||
try {
|
||||
yield ThemingApi.deleteTheme(themeId);
|
||||
yield AppThemingApi.deleteTheme(themeId);
|
||||
|
||||
yield put({
|
||||
type: ReduxActionTypes.DELETE_APP_THEME_SUCCESS,
|
||||
|
|
@ -289,15 +292,15 @@ function* setDefaultSelectedThemeOnError() {
|
|||
|
||||
try {
|
||||
// Fetch all system themes
|
||||
const response: ApiResponse<AppTheme[]> =
|
||||
yield ThemingApi.fetchThemes(applicationId);
|
||||
const response: SagaReturnType<typeof AppThemingApi.fetchThemes> =
|
||||
yield AppThemingApi.fetchThemes(applicationId);
|
||||
|
||||
// Gets default theme
|
||||
const theme = find(response.data, { name: "Default" });
|
||||
|
||||
if (theme) {
|
||||
// Update API call to set current theme to default
|
||||
yield ThemingApi.changeTheme(applicationId, theme);
|
||||
yield AppThemingApi.changeTheme(applicationId, theme);
|
||||
yield put({
|
||||
type: ReduxActionTypes.FETCH_SELECTED_APP_THEME_SUCCESS,
|
||||
payload: theme,
|
||||
|
|
|
|||
|
|
@ -401,7 +401,9 @@ function* handleDatasourceDeleteRedirect(deletedDatasourceId: string) {
|
|||
|
||||
// Go to the add datasource if the last item is deleted
|
||||
if (remainingDatasources.length === 0) {
|
||||
history.push(integrationEditorURL({ selectedTab: INTEGRATION_TABS.NEW }));
|
||||
yield call(() =>
|
||||
history.push(integrationEditorURL({ selectedTab: INTEGRATION_TABS.NEW })),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import {
|
|||
import store from "store";
|
||||
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { axiosConnectionAbortedCode } from "ee/api/ApiUtils";
|
||||
import { AXIOS_CONNECTION_ABORTED_CODE } from "ee/constants/ApiConstants";
|
||||
import { getLoginUrl } from "ee/utils/adminSettingsHelpers";
|
||||
import type { PluginErrorDetails } from "api/ActionAPI";
|
||||
import showToast from "sagas/ToastSagas";
|
||||
|
|
@ -104,7 +104,7 @@ export function* validateResponse(
|
|||
}
|
||||
|
||||
// letting `apiFailureResponseInterceptor` handle it this case
|
||||
if (response?.code === axiosConnectionAbortedCode) {
|
||||
if (response?.code === AXIOS_CONNECTION_ABORTED_CODE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -81,8 +81,8 @@ import type { FetchPageResponse, FetchPageResponseData } from "api/PageApi";
|
|||
import type { AppTheme } from "entities/AppTheming";
|
||||
import type { Datasource } from "entities/Datasource";
|
||||
import type { Plugin, PluginFormPayload } from "api/PluginApi";
|
||||
import ConsolidatedPageLoadApi from "api/ConsolidatedPageLoadApi";
|
||||
import { axiosConnectionAbortedCode } from "ee/api/ApiUtils";
|
||||
import { ConsolidatedPageLoadApi } from "api";
|
||||
import { AXIOS_CONNECTION_ABORTED_CODE } from "ee/constants/ApiConstants";
|
||||
import {
|
||||
endSpan,
|
||||
startNestedSpan,
|
||||
|
|
@ -254,7 +254,7 @@ export function* getInitResponses({
|
|||
|
||||
if (!isValidResponse) {
|
||||
// its only invalid when there is a axios related error
|
||||
throw new Error("Error occured " + axiosConnectionAbortedCode);
|
||||
throw new Error("Error occured " + AXIOS_CONNECTION_ABORTED_CODE);
|
||||
}
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import set from "lodash/set";
|
|||
import log from "loglevel";
|
||||
import { isPlainObject, isString } from "lodash";
|
||||
import { DATA_BIND_REGEX_GLOBAL } from "constants/BindingsConstants";
|
||||
import { apiFailureResponseInterceptor } from "ee/api/ApiUtils";
|
||||
import { apiFailureResponseInterceptor } from "api/interceptors";
|
||||
import { klonaLiteWithTelemetry } from "utils/helpers";
|
||||
|
||||
// function to extract all objects that have dynamic values
|
||||
|
|
@ -237,7 +237,9 @@ export function* getFromServerWhenNoPrefetchedResult(
|
|||
},
|
||||
status,
|
||||
},
|
||||
});
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user