From f5a361631dad082dd9bc9c99b391a2a559adaf24 Mon Sep 17 00:00:00 2001 From: James Mead Date: Thu, 6 Jun 2024 17:31:46 +0100 Subject: [PATCH] WIP: Encapsulate axios calls in an Api class So we only need to set the host/baseURL in one place. This should make it easier to pull the host/baseURL (i.e. the value of the `REACT_APP_API_ENDPOINT` env var) up into an attribute on the web component. --- .../ImageUploadButton/ImageUploadButton.jsx | 5 +- .../Runners/PyodideRunner/PyodideRunner.jsx | 5 +- .../Runners/PythonRunner/PythonRunner.jsx | 5 +- src/components/Modals/NewProjectModal.jsx | 5 +- src/utils/apiCallHandler.js | 236 +++++++++--------- 5 files changed, 133 insertions(+), 123 deletions(-) diff --git a/src/components/Editor/ImageUploadButton/ImageUploadButton.jsx b/src/components/Editor/ImageUploadButton/ImageUploadButton.jsx index 11ac2fdf4..0489a6c77 100644 --- a/src/components/Editor/ImageUploadButton/ImageUploadButton.jsx +++ b/src/components/Editor/ImageUploadButton/ImageUploadButton.jsx @@ -9,7 +9,7 @@ import { updateImages, setNameError } from "../../../redux/EditorSlice"; import Button from "../../Button/Button"; import NameErrorMessage from "../ErrorMessage/NameErrorMessage"; import store from "../../../app/store"; -import { uploadImages } from "../../../utils/apiCallHandler"; +import { Api } from "../../../utils/apiCallHandler"; const allowedExtensions = { python: ["jpg", "jpeg", "png", "gif"], @@ -31,6 +31,7 @@ const allowedExtensionsString = (projectType) => { }; const ImageUploadButton = () => { + const api = new Api(); const [modalIsOpen, setIsOpen] = useState(false); const [files, setFiles] = useState([]); const dispatch = useDispatch(); @@ -75,7 +76,7 @@ const ImageUploadButton = () => { } }); if (store.getState().editor.nameError === "") { - const response = await uploadImages( + const response = await api.uploadImages( projectIdentifier, user.access_token, files, diff --git a/src/components/Editor/Runners/PyodideRunner/PyodideRunner.jsx b/src/components/Editor/Runners/PyodideRunner/PyodideRunner.jsx index 7a7a9906e..182aeaae2 100644 --- a/src/components/Editor/Runners/PyodideRunner/PyodideRunner.jsx +++ b/src/components/Editor/Runners/PyodideRunner/PyodideRunner.jsx @@ -14,7 +14,7 @@ import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; import { useMediaQuery } from "react-responsive"; import { MOBILE_MEDIA_QUERY } from "../../../../utils/mediaQueryBreakpoints"; import ErrorMessage from "../../ErrorMessage/ErrorMessage"; -import { createError } from "../../../../utils/apiCallHandler"; +import { Api } from "../../../../utils/apiCallHandler"; import VisualOutputPane from "./VisualOutputPane"; import OutputViewToggle from "../PythonRunner/OutputViewToggle"; import { SettingsContext } from "../../../../utils/settings"; @@ -25,6 +25,7 @@ const PyodideRunner = () => { () => new Worker("./PyodideWorker.js", { type: "module" }), [], ); + const api = new Api(); const interruptBuffer = useRef(); const stdinBuffer = useRef(); const stdinClosed = useRef(); @@ -139,7 +140,7 @@ const PyodideRunner = () => { node.scrollTop = node.scrollHeight; }; - const handleError = (file, line, mistake, type, info) => { + const handleError = async (file, line, mistake, type, info) => { let errorMessage; if (type === "KeyboardInterrupt") { diff --git a/src/components/Editor/Runners/PythonRunner/PythonRunner.jsx b/src/components/Editor/Runners/PythonRunner/PythonRunner.jsx index 6ba83c8e9..7934712b9 100644 --- a/src/components/Editor/Runners/PythonRunner/PythonRunner.jsx +++ b/src/components/Editor/Runners/PythonRunner/PythonRunner.jsx @@ -15,7 +15,7 @@ import { triggerDraw, } from "../../../../redux/EditorSlice"; import ErrorMessage from "../../ErrorMessage/ErrorMessage"; -import { createError } from "../../../../utils/apiCallHandler"; +import { Api } from "../../../../utils/apiCallHandler"; import store from "../../../../app/store"; import VisualOutputPane from "./VisualOutputPane"; import OutputViewToggle from "./OutputViewToggle"; @@ -55,6 +55,7 @@ const externalLibraries = { }; const PythonRunner = ({ outputPanels = ["text", "visual"] }) => { + const api = new Api(); const projectCode = useSelector((state) => state.editor.project.components); const projectIdentifier = useSelector( (state) => state.editor.project.identifier, @@ -301,7 +302,7 @@ const PythonRunner = ({ outputPanels = ["text", "visual"] }) => { } errorMessage = `${errorType}: ${errorDescription} on line ${lineNumber} of ${fileName}`; - createError(projectIdentifier, userId, { + api.createError(projectIdentifier, userId, { errorType, errorMessage, }); diff --git a/src/components/Modals/NewProjectModal.jsx b/src/components/Modals/NewProjectModal.jsx index c80ddc64f..5d937a217 100644 --- a/src/components/Modals/NewProjectModal.jsx +++ b/src/components/Modals/NewProjectModal.jsx @@ -5,13 +5,14 @@ import { closeNewProjectModal } from "../../redux/EditorSlice"; import { useDispatch, useSelector } from "react-redux"; import { useTranslation } from "react-i18next"; import InputModal from "./InputModal"; -import { createOrUpdateProject } from "../../utils/apiCallHandler"; +import { Api } from "../../utils/apiCallHandler"; import { useNavigate } from "react-router-dom"; import { DEFAULT_PROJECTS } from "../../utils/defaultProjects"; import HTMLIcon from "../../assets/icons/html.svg"; import PythonIcon from "../../assets/icons/python.svg"; const NewProjectModal = () => { + const api = new Api(); const { t, i18n } = useTranslation(); const dispatch = useDispatch(); const user = useSelector((state) => state.auth.user); @@ -29,7 +30,7 @@ const NewProjectModal = () => { const navigate = useNavigate(); const createProject = async () => { - const response = await createOrUpdateProject( + const response = await api.createOrUpdateProject( { ...DEFAULT_PROJECTS[projectType], name: projectName }, user.access_token, ); diff --git a/src/utils/apiCallHandler.js b/src/utils/apiCallHandler.js index 3294ab673..de0deef22 100644 --- a/src/utils/apiCallHandler.js +++ b/src/utils/apiCallHandler.js @@ -1,124 +1,130 @@ import axios from "axios"; import omit from "lodash/omit"; -const host = process.env.REACT_APP_API_ENDPOINT; - -const get = async (url, headers) => { - return await axios.get(url, headers); -}; - -const post = async (url, body, headers) => { - return await axios.post(url, body, headers); -}; - -const put = async (url, body, headers) => { - return await axios.put(url, body, headers); -}; - -const headers = (accessToken) => { - let headersHash; - if (accessToken) { - headersHash = { Accept: "application/json", Authorization: accessToken }; - } else { - headersHash = { Accept: "application/json" }; - } - return { headers: headersHash }; -}; - -export const createOrUpdateProject = async (projectWithUserId, accessToken) => { - const project = omit(projectWithUserId, ["user_id"]); - if (!project.identifier) { +export class Api { + api; + + constructor() { + this.api = axios.create({ baseURL: process.env.REACT_APP_API_ENDPOINT }); + }; + + async createOrUpdateProject (projectWithUserId, accessToken) { + const project = omit(projectWithUserId, ["user_id"]); + if (!project.identifier) { + return await post( + `${host}/api/projects`, + { project }, + headers(accessToken), + ); + } else { + return await put( + `${host}/api/projects/${project.identifier}`, + { project }, + headers(accessToken), + ); + } + }; + + async deleteProject(identifier, accessToken) { + return await axios.delete( + `${host}/api/projects/${identifier}`, + headers(accessToken), + ); + }; + + async getImage(url) { + return await get(url, headers()); + }; + + async loadRemix(projectIdentifier, accessToken) { + return await get( + `${host}/api/projects/${projectIdentifier}/remix`, + headers(accessToken), + ); + }; + + async createRemix(project, accessToken) { return await post( - `${host}/api/projects`, + `${host}/api/projects/${project.identifier}/remix`, { project }, headers(accessToken), ); - } else { - return await put( - `${host}/api/projects/${project.identifier}`, - { project }, + }; + + async readProject(projectIdentifier, locale, accessToken) { + const queryString = locale ? `?locale=${locale}` : ""; + return await get( + `${host}/api/projects/${projectIdentifier}${queryString}`, headers(accessToken), ); - } -}; - -export const deleteProject = async (identifier, accessToken) => { - return await axios.delete( - `${host}/api/projects/${identifier}`, - headers(accessToken), - ); -}; - -export const getImage = async (url) => { - return await get(url, headers()); -}; - -export const loadRemix = async (projectIdentifier, accessToken) => { - return await get( - `${host}/api/projects/${projectIdentifier}/remix`, - headers(accessToken), - ); -}; - -export const createRemix = async (project, accessToken) => { - return await post( - `${host}/api/projects/${project.identifier}/remix`, - { project }, - headers(accessToken), - ); -}; - -export const readProject = async (projectIdentifier, locale, accessToken) => { - const queryString = locale ? `?locale=${locale}` : ""; - return await get( - `${host}/api/projects/${projectIdentifier}${queryString}`, - headers(accessToken), - ); -}; - -export const loadAssets = async (assetsIdentifier, locale, accessToken) => { - const queryString = locale ? `?locale=${locale}` : ""; - return await get( - `${host}/api/projects/${assetsIdentifier}/images${queryString}`, - headers(accessToken), - ); -}; - -export const readProjectList = async (page, accessToken) => { - return await get(`${host}/api/projects`, { - params: { page }, - ...headers(accessToken), - }); -}; - -export const uploadImages = async (projectIdentifier, accessToken, images) => { - var formData = new FormData(); - - images.forEach((image) => { - formData.append("images[]", image, image.name); - }); - - return await post( - `${host}/api/projects/${projectIdentifier}/images`, - formData, - { ...headers(accessToken), "Content-Type": "multipart/form-data" }, - ); -}; - -export const createError = async ( - projectIdentifier, - userId, - error, - sendError = false, -) => { - if (!sendError) { - return; - } - const { errorMessage, errorType } = error; - return await post(`${host}/api/project_errors`, { - error: errorMessage, - error_type: errorType, - project_id: projectIdentifier, - user_id: userId, - }); -}; + }; + + async loadAssets(assetsIdentifier, locale, accessToken) { + const queryString = locale ? `?locale=${locale}` : ""; + return await get( + `${host}/api/projects/${assetsIdentifier}/images${queryString}`, + headers(accessToken), + ); + }; + + async readProjectList(page, accessToken) { + return await get(`${host}/api/projects`, { + params: { page }, + ...headers(accessToken), + }); + }; + + async uploadImages(projectIdentifier, accessToken, images) { + var formData = new FormData(); + + images.forEach((image) => { + formData.append("images[]", image, image.name); + }); + + return await post( + `${host}/api/projects/${projectIdentifier}/images`, + formData, + { ...headers(accessToken), "Content-Type": "multipart/form-data" }, + ); + }; + + async createError( + projectIdentifier, + userId, + error, + sendError = false, + ) { + if (!sendError) { + return; + } + const { errorMessage, errorType } = error; + return await post(`${host}/api/project_errors`, { + error: errorMessage, + error_type: errorType, + project_id: projectIdentifier, + user_id: userId, + }); + }; + + async #get(url, headers) { + return await axios.get(url, headers); + }; + + async #post(url, body, headers) { + return await axios.post(url, body, headers); + }; + + async #put(url, body, headers) { + return await axios.put(url, body, headers); + }; + + #headers(accessToken) { + let headersHash; + if (accessToken) { + headersHash = { Accept: "application/json", Authorization: accessToken }; + } else { + headersHash = { Accept: "application/json" }; + } + return { headers: headersHash }; + }; +}