From 5fdc86cd52396588a1e72c141f4abee282e91a46 Mon Sep 17 00:00:00 2001 From: reglim Date: Wed, 18 Jan 2023 16:40:33 +0100 Subject: [PATCH] Add option for a prefix path You can now add a prefix path for docat (eg. https://domain.com/docat as the root url). Just follow the instructions in the readme fixes: #80 --- Dockerfile | 19 ++-- README.md | 46 +++++++++- docat/README.md | 4 +- docat/docat/app.py | 68 ++++++++------ docat/docat/nginx/default | 14 +-- web/package.json | 2 +- web/src/components/SearchBar.tsx | 19 ++-- web/src/data-providers/ConfigDataProvider.tsx | 8 +- .../data-providers/ProjectDataProvider.tsx | 4 +- web/src/pages/Docs.tsx | 19 ++-- web/src/pages/Help.tsx | 5 +- web/src/pages/Search.tsx | 2 +- web/src/repositories/ProjectRepository.ts | 49 +++++++++-- web/src/setupProxy.js | 5 +- .../repositories/ProjectRepository.test.ts | 88 +++++++++++++++++-- 15 files changed, 272 insertions(+), 80 deletions(-) diff --git a/Dockerfile b/Dockerfile index 169e577c8..70f1c75fc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,12 @@ + # building frontend FROM node:16.14 as frontend WORKDIR /app/frontend + +ARG PREFIX_PATH_ARG="" +ENV PREFIX_PATH=$PREFIX_PATH_ARG +ENV REACT_APP_PREFIX_PATH=$PREFIX_PATH_ARG + COPY web ./ # fix docker not following symlinks @@ -12,7 +18,6 @@ RUN yarn lint # fix test not exiting by default ARG CI=true RUN yarn test - RUN yarn build # setup Python @@ -37,21 +42,25 @@ RUN poetry install --no-root --no-ansi --only main # production FROM python:3.11.0-alpine3.15 +ARG PREFIX_PATH_ARG="" +ENV PREFIX_PATH=$PREFIX_PATH_ARG # set up the system RUN apk update && \ - apk add nginx dumb-init libmagic && \ + apk add nginx dumb-init libmagic gettext && \ rm -rf /var/cache/apk/* RUN mkdir -p /var/docat/doc # install the application -RUN mkdir -p /var/www/html -COPY --from=frontend /app/frontend/build /var/www/html +RUN mkdir -p /var/www/html${PREFIX_PATH} +COPY --from=frontend /app/frontend/build /var/www/html${PREFIX_PATH} COPY docat /app/docat WORKDIR /app/docat -RUN cp docat/nginx/default /etc/nginx/http.d/default.conf + +# substitute the prefix path in the nginx config +RUN envsubst < docat/nginx/default > /etc/nginx/http.d/default.conf # Copy the build artifact (.venv) COPY --from=backend /app /app/docat diff --git a/README.md b/README.md index 59756f192..3bfccc642 100644 --- a/README.md +++ b/README.md @@ -132,4 +132,48 @@ Using `docatl`: docatl update-index --host http://localhost:8000 ``` -Don't worry if it takes some time :) \ No newline at end of file +Don't worry if it takes some time :) + +## Adding a prefix path + +It is possible to run docat on a subpath, for example `/docat/...`. To do this, you will need to perform different steps depending on how you want to run docat. + +### Common Configuration + +The first thing you will have to do no matter how you start docat, is to specify the prefix path in the `package.json` file (underneath the `version` tag): + +> Important: The prefix path must start with a slash and end without one. (eg. `/docat`) + +```json +"homepage": "/docat", +``` + +### Docker + +To run docat with a prefix path in docker, you will need to rebuild the container with the build argument **PREFIX_PATH_ARG** set to your prefix path: + +```sh +docker build . --no-cache --tag docat:with_prefix --build-arg PREFIX_PATH_ARG="/docat" + +docker run --volume $DEV_DOCAT_PATH:/var/docat/ --publish 80:80 docat:with_prefix +``` + +### Local + +To run docat locally with a prefix path, you have to specify the prefix path in environment variables for both the backend and the frontend: + +#### Backend + +You can run the backend directly with the **PREFIX_PATH** argument: + +```sh +DOCAT_SERVE_FILES=1 DOCAT_STORAGE_PATH="$DEV_DOCAT_PATH" PREFIX_PATH="/docat" poetry run python -m docat +``` + +#### Frontend + +For the frontend, you can just add the **REACT_APP_PREFIX_PATH** environment variable into the `.env` file: + +``` +REACT_APP_PREFIX_PATH=/docat +``` \ No newline at end of file diff --git a/docat/README.md b/docat/README.md index 2d72a54bb..ef512a6f9 100644 --- a/docat/README.md +++ b/docat/README.md @@ -13,7 +13,7 @@ Install the dependencies and run the application: # install dependencies poetry install # run the app -[DOCAT_SERVE_FILES=1] [DOCAT_INDEX_FILES=1] [FLASK_DEBUG=1] [PORT=8888] poetry run python -m docat +[DOCAT_SERVE_FILES=1] [DOCAT_INDEX_FILES=1] [PORT=8888] poetry run python -m docat ``` ### Config Options @@ -21,7 +21,7 @@ poetry install * **DOCAT_SERVE_FILES**: Serve static documentation instead of a nginx (for testing) * **DOCAT_INDEX_FILES**: Index files on start for searching * **DOCAT_STORAGE_PATH**: Upload directory for static files (needs to match nginx config) -* **FLASK_DEBUG**: Start flask in debug mode +* **PREFIX_PATH**: Start FastAPI with a prefix path (eg. `/docat/api/projects/...`). -> needs to start with a slash and end without one ## Usage diff --git a/docat/docat/app.py b/docat/docat/app.py index ea0981129..19bc96122 100644 --- a/docat/docat/app.py +++ b/docat/docat/app.py @@ -14,7 +14,16 @@ from typing import Optional import magic -from fastapi import Depends, FastAPI, File, Header, Response, UploadFile, status +from fastapi import ( + APIRouter, + Depends, + FastAPI, + File, + Header, + Response, + UploadFile, + status, +) from fastapi.staticfiles import StaticFiles from starlette.responses import JSONResponse from tinydb import Query, TinyDB @@ -48,7 +57,10 @@ update_version_index_for_project, ) -#: Holds the FastAPI application +PREFIX_PATH = os.getenv("PREFIX_PATH", "") + +router = APIRouter(prefix=PREFIX_PATH) + app = FastAPI( title="docat", description="API for docat, https://github.com/docat-org/docat", @@ -63,7 +75,7 @@ DOCAT_UPLOAD_FOLDER = DOCAT_STORAGE_PATH / UPLOAD_FOLDER -@app.on_event("startup") +@router.on_event("startup") def startup_create_folders(): # Create the folders if they don't exist DOCAT_UPLOAD_FOLDER.mkdir(parents=True, exist_ok=True) @@ -79,28 +91,28 @@ def get_index_db() -> TinyDB: return TinyDB(DOCAT_INDEX_PATH) -@app.post("/api/index/update", response_model=ApiResponse, status_code=status.HTTP_200_OK) -@app.post("/api/index/update/", response_model=ApiResponse, status_code=status.HTTP_200_OK) +@router.post("/api/index/update", response_model=ApiResponse, status_code=status.HTTP_200_OK) +@router.post("/api/index/update/", response_model=ApiResponse, status_code=status.HTTP_200_OK) def update_index(): index_all_projects(DOCAT_UPLOAD_FOLDER, DOCAT_INDEX_PATH) return ApiResponse(message="Successfully updated search index") -@app.get("/api/projects", response_model=Projects, status_code=status.HTTP_200_OK) +@router.get("/api/projects", response_model=Projects, status_code=status.HTTP_200_OK) def get_projects(include_hidden: bool = False): if not DOCAT_UPLOAD_FOLDER.exists(): return Projects(projects=[]) return get_all_projects(DOCAT_UPLOAD_FOLDER, include_hidden) -@app.get( +@router.get( "/api/projects/{project}", response_model=ProjectDetail, status_code=status.HTTP_200_OK, responses={status.HTTP_404_NOT_FOUND: {"model": ApiResponse}}, ) -@app.get( +@router.get( "/api/projects/{project}/", response_model=ProjectDetail, status_code=status.HTTP_200_OK, @@ -115,8 +127,8 @@ def get_project(project, include_hidden: bool = False): return details -@app.get("/api/search", response_model=SearchResponse, status_code=status.HTTP_200_OK) -@app.get("/api/search/", response_model=SearchResponse, status_code=status.HTTP_200_OK) +@router.get("/api/search", response_model=SearchResponse, status_code=status.HTTP_200_OK) +@router.get("/api/search/", response_model=SearchResponse, status_code=status.HTTP_200_OK) def search(query: str, index_db: TinyDB = Depends(get_index_db)): query = query.lower().strip() @@ -193,8 +205,8 @@ def search(query: str, index_db: TinyDB = Depends(get_index_db)): return SearchResponse(projects=found_projects, versions=found_versions, files=found_files) -@app.post("/api/{project}/icon", response_model=ApiResponse, status_code=status.HTTP_200_OK) -@app.post("/api/{project}/icon/", response_model=ApiResponse, status_code=status.HTTP_200_OK) +@router.post("/api/{project}/icon", response_model=ApiResponse, status_code=status.HTTP_200_OK) +@router.post("/api/{project}/icon/", response_model=ApiResponse, status_code=status.HTTP_200_OK) def upload_icon( project: str, response: Response, @@ -234,8 +246,8 @@ def upload_icon( return ApiResponse(message="Icon successfully uploaded") -@app.post("/api/{project}/{version}/hide", response_model=ApiResponse, status_code=status.HTTP_200_OK) -@app.post("/api/{project}/{version}/hide/", response_model=ApiResponse, status_code=status.HTTP_200_OK) +@router.post("/api/{project}/{version}/hide", response_model=ApiResponse, status_code=status.HTTP_200_OK) +@router.post("/api/{project}/{version}/hide/", response_model=ApiResponse, status_code=status.HTTP_200_OK) def hide_version( project: str, version: str, @@ -274,8 +286,8 @@ def hide_version( return ApiResponse(message=f"Version {version} is now hidden") -@app.post("/api/{project}/{version}/show", response_model=ApiResponse, status_code=status.HTTP_200_OK) -@app.post("/api/{project}/{version}/show/", response_model=ApiResponse, status_code=status.HTTP_200_OK) +@router.post("/api/{project}/{version}/show", response_model=ApiResponse, status_code=status.HTTP_200_OK) +@router.post("/api/{project}/{version}/show/", response_model=ApiResponse, status_code=status.HTTP_200_OK) def show_version( project: str, version: str, @@ -313,8 +325,8 @@ def show_version( return ApiResponse(message=f"Version {version} is now shown") -@app.post("/api/{project}/{version}", response_model=ApiResponse, status_code=status.HTTP_201_CREATED) -@app.post("/api/{project}/{version}/", response_model=ApiResponse, status_code=status.HTTP_201_CREATED) +@router.post("/api/{project}/{version}", response_model=ApiResponse, status_code=status.HTTP_201_CREATED) +@router.post("/api/{project}/{version}/", response_model=ApiResponse, status_code=status.HTTP_201_CREATED) def upload( project: str, version: str, @@ -361,8 +373,8 @@ def upload( return ApiResponse(message="File successfully uploaded") -@app.put("/api/{project}/{version}/tags/{new_tag}", response_model=ApiResponse, status_code=status.HTTP_201_CREATED) -@app.put("/api/{project}/{version}/tags/{new_tag}/", response_model=ApiResponse, status_code=status.HTTP_201_CREATED) +@router.put("/api/{project}/{version}/tags/{new_tag}", response_model=ApiResponse, status_code=status.HTTP_201_CREATED) +@router.put("/api/{project}/{version}/tags/{new_tag}/", response_model=ApiResponse, status_code=status.HTTP_201_CREATED) def tag(project: str, version: str, new_tag: str, response: Response, index_db: TinyDB = Depends(get_index_db)): destination = DOCAT_UPLOAD_FOLDER / project / new_tag source = DOCAT_UPLOAD_FOLDER / project / version @@ -380,13 +392,13 @@ def tag(project: str, version: str, new_tag: str, response: Response, index_db: return ApiResponse(message=f"Tag {new_tag} -> {version} successfully created") -@app.get( +@router.get( "/api/{project}/claim", response_model=ClaimResponse, status_code=status.HTTP_201_CREATED, responses={status.HTTP_409_CONFLICT: {"model": ApiResponse}}, ) -@app.get( +@router.get( "/api/{project}/claim/", response_model=ClaimResponse, status_code=status.HTTP_201_CREATED, @@ -407,8 +419,8 @@ def claim(project: str, db: TinyDB = Depends(get_db)): return ClaimResponse(message=f"Project {project} successfully claimed", token=token) -@app.put("/api/{project}/rename/{new_project_name}", response_model=ApiResponse, status_code=status.HTTP_200_OK) -@app.put("/api/{project}/rename/{new_project_name}/", response_model=ApiResponse, status_code=status.HTTP_200_OK) +@router.put("/api/{project}/rename/{new_project_name}", response_model=ApiResponse, status_code=status.HTTP_200_OK) +@router.put("/api/{project}/rename/{new_project_name}/", response_model=ApiResponse, status_code=status.HTTP_200_OK) def rename( project: str, new_project_name: str, @@ -458,8 +470,8 @@ def rename( return ApiResponse(message=f"Successfully renamed project {project} to {new_project_name}") -@app.delete("/api/{project}/{version}", response_model=ApiResponse, status_code=status.HTTP_200_OK) -@app.delete("/api/{project}/{version}/", response_model=ApiResponse, status_code=status.HTTP_200_OK) +@router.delete("/api/{project}/{version}", response_model=ApiResponse, status_code=status.HTTP_200_OK) +@router.delete("/api/{project}/{version}/", response_model=ApiResponse, status_code=status.HTTP_200_OK) def delete( project: str, version: str, @@ -501,8 +513,10 @@ def check_token_for_project(db, token, project) -> TokenStatus: # serve_local_docs for local testing without a nginx if os.environ.get("DOCAT_SERVE_FILES"): DOCAT_UPLOAD_FOLDER.mkdir(parents=True, exist_ok=True) - app.mount("/doc", StaticFiles(directory=DOCAT_UPLOAD_FOLDER, html=True), name="docs") + app.mount(f"{PREFIX_PATH}/doc", StaticFiles(directory=DOCAT_UPLOAD_FOLDER, html=True), name="docs") # index local files on start if os.environ.get("DOCAT_INDEX_FILES"): index_all_projects(DOCAT_UPLOAD_FOLDER, DOCAT_INDEX_PATH) + +app.include_router(router) diff --git a/docat/docat/nginx/default b/docat/docat/nginx/default index 712cb2869..0a4614c46 100644 --- a/docat/docat/nginx/default +++ b/docat/docat/nginx/default @@ -13,15 +13,19 @@ server { server_name _; - location /doc { - root /var/docat; + location $PREFIX_PATH/doc { + # remove $PREFIX_PATH/doc from the request + rewrite ^$PREFIX_PATH/doc(.*) $1 break; + + root /var/docat/doc; } - location /api { + location $PREFIX_PATH/api { client_max_body_size 100M; proxy_pass http://python_backend; } - location / { + location $PREFIX_PATH/ { + } -} +} \ No newline at end of file diff --git a/web/package.json b/web/package.json index 2c69116c5..7ae7ce571 100644 --- a/web/package.json +++ b/web/package.json @@ -61,4 +61,4 @@ "eslint-plugin-promise": "^6.0.0", "eslint-plugin-react": "^7.31.11" } -} +} \ No newline at end of file diff --git a/web/src/components/SearchBar.tsx b/web/src/components/SearchBar.tsx index 658518bd0..dfeec4023 100644 --- a/web/src/components/SearchBar.tsx +++ b/web/src/components/SearchBar.tsx @@ -1,19 +1,22 @@ import { TextField } from '@mui/material' -import { useNavigate } from 'react-router-dom' +import { Link } from 'react-router-dom' import React from 'react' import styles from '../style/components/SearchBar.module.css' import { Search } from '@mui/icons-material' export default function SearchBar(): JSX.Element { - const navigate = useNavigate() + const linkRef = React.useRef(null) const [searchQuery, setSearchQuery] = React.useState('') + const navigate = (): void => { + if (linkRef.current != null) { + linkRef.current.click() + } + } + return ( <> - navigate('/search')} - /> + navigate()} />
setSearchQuery(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { - navigate(`/search?query=${searchQuery}`) + navigate() } }} variant="standard" >
+ + ) } diff --git a/web/src/data-providers/ConfigDataProvider.tsx b/web/src/data-providers/ConfigDataProvider.tsx index c27272f33..4f7deae23 100644 --- a/web/src/data-providers/ConfigDataProvider.tsx +++ b/web/src/data-providers/ConfigDataProvider.tsx @@ -5,7 +5,7 @@ */ import React, { createContext, useContext, useEffect, useState } from 'react' - +import ProjectRepository from '../repositories/ProjectRepository' export interface Config { headerHTML?: string } @@ -22,8 +22,10 @@ export const ConfigDataProvider = ({ children }: any): JSX.Element => { useEffect(() => { void (async () => { try { - const res = await fetch('/doc/config.json') - const data = await res.json() as Config + const res = await fetch( + `${ProjectRepository.getURLPrefix()}/doc/config.json` + ) + const data = (await res.json()) as Config setConfig(data) } catch (err) { console.error(err) diff --git a/web/src/data-providers/ProjectDataProvider.tsx b/web/src/data-providers/ProjectDataProvider.tsx index 736fa295c..24f8d8c1e 100644 --- a/web/src/data-providers/ProjectDataProvider.tsx +++ b/web/src/data-providers/ProjectDataProvider.tsx @@ -38,7 +38,9 @@ export function ProjectDataProvider({ children }: any): JSX.Element { const loadData = (): void => { void (async (): Promise => { try { - const response = await fetch('/api/projects?include_hidden=true') + const response = await fetch( + `${ProjectRepository.getURLPrefix()}/api/projects?include_hidden=true` + ) if (!response.ok) { throw new Error( diff --git a/web/src/pages/Docs.tsx b/web/src/pages/Docs.tsx index 750b36250..b8ac62abe 100644 --- a/web/src/pages/Docs.tsx +++ b/web/src/pages/Docs.tsx @@ -44,16 +44,19 @@ export default function Docs(): JSX.Element { page: string, hideControls: boolean ): void => { - const newState = `/#/${project}/${version}/${page}${ - hideControls ? '?hide-ui=true' : '' - }` + const oldUrl = window.location.href + + const newUrl = ProjectRepository.getDocPageURL( + oldUrl, + project, + version, + page, + hideControls + ) - // skip updating the route if the new state is the same as the current one - if (window.location.hash === newState.substring(1)) { - return + if (newUrl !== oldUrl) { + window.history.pushState({}, '', newUrl) } - - window.history.pushState({}, '', newState) }, [] ) diff --git a/web/src/pages/Help.tsx b/web/src/pages/Help.tsx index 96327ffef..0a3f64fc7 100644 --- a/web/src/pages/Help.tsx +++ b/web/src/pages/Help.tsx @@ -9,8 +9,9 @@ import Header from '../components/Header' import LoadingPage from './LoadingPage' import styles from './../style/pages/Help.module.css' +import ProjectRepository from '../repositories/ProjectRepository' -export default function Help (): JSX.Element { +export default function Help(): JSX.Element { document.title = 'Help | docat' const [content, setContent] = useState('') @@ -27,7 +28,7 @@ export default function Help (): JSX.Element { const port = document.location.port !== '' ? `:${document.location.port}` : '' - const currentUrl = `${protocol}//${host}${port}` + const currentUrl = `${protocol}//${host}${port}/${ProjectRepository.getURLPrefix()}` return text.replaceAll('http://localhost:8000', currentUrl) } diff --git a/web/src/pages/Search.tsx b/web/src/pages/Search.tsx index 591d3b124..a525ee8cf 100644 --- a/web/src/pages/Search.tsx +++ b/web/src/pages/Search.tsx @@ -55,7 +55,7 @@ export default function Search(): JSX.Element { useEffect(() => { searchDebounced.cancel() - window.history.pushState({}, '', `/#/search?query=${searchQuery}`) + window.history.pushState({}, '', `${ProjectRepository.getURLPrefix()}/#/search?query=${searchQuery}`) searchDebounced() }, [searchQuery]) diff --git a/web/src/repositories/ProjectRepository.ts b/web/src/repositories/ProjectRepository.ts index 579b213ab..000a844bd 100644 --- a/web/src/repositories/ProjectRepository.ts +++ b/web/src/repositories/ProjectRepository.ts @@ -5,7 +5,7 @@ import { ApiSearchResponse } from '../models/SearchResult' const RESOURCE = 'doc' -function filterHiddenVersions(allProjects: Project[]): Project[] { +function filterHiddenVersions (allProjects: Project[]): Project[] { // create deep-copy first const projects = JSON.parse(JSON.stringify(allProjects)) as Project[] @@ -21,7 +21,7 @@ function filterHiddenVersions(allProjects: Project[]): Project[] { * @param {string} projectName Name of the project */ async function getVersions (projectName: string): Promise { - const res = await fetch(`/api/projects/${projectName}?include_hidden=true`) + const res = await fetch(`${getURLPrefix()}/api/projects/${projectName}?include_hidden=true`) if (!res.ok) { console.error((await res.json() as { message: string }).message) @@ -41,7 +41,7 @@ async function getVersions (projectName: string): Promise { * @returns */ async function search (query: string): Promise { - const response = await fetch(`/api/search?query=${query}`) + const response = await fetch(`${getURLPrefix()}/api/search?query=${query}`) if (response.ok) { return await response.json() as ApiSearchResponse @@ -60,7 +60,7 @@ async function search (query: string): Promise { * @param {string} projectName Name of the project */ function getProjectLogoURL (projectName: string): string { - return `/${RESOURCE}/${projectName}/logo` + return `${getURLPrefix()}/${RESOURCE}/${projectName}/logo` } /** @@ -70,7 +70,28 @@ function getProjectLogoURL (projectName: string): string { * @param {string?} docsPath Path to the documentation page */ function getProjectDocsURL (projectName: string, version: string, docsPath?: string): string { - return `/${RESOURCE}/${projectName}/${version}/${docsPath ?? ''}` + return `${getURLPrefix()}/${RESOURCE}/${projectName}/${version}/${docsPath ?? ''}` +} + +/** + * Returns the new URL used on the Docs page + * @param {string} currentURL Current URL + * @param {string} project Name of the project + * @param {string} version Name of the version + * @param {string} path Path to the documentation page + * @param {boolean} hideControls Whether to hide the controls + * @returns {string} New URL +*/ +function getDocPageURL (currentURL: string, project: string, version: string, path: string, hideControls: boolean): string { + if (path.startsWith('/')) { + path = path.slice(1) + } + + const startOfParams = currentURL.indexOf('#') + 1 + const oldParams = currentURL.slice(startOfParams) + const newParams = `/${project}/${version}/${path}${hideControls ? '?hide-ui=true' : ''}` + + return currentURL.replace(oldParams, newParams) } /** @@ -80,7 +101,7 @@ function getProjectDocsURL (projectName: string, version: string, docsPath?: str * @param {FormData} body Data to upload */ async function upload (projectName: string, version: string, body: FormData): Promise { - const resp = await fetch(`/api/${projectName}/${version}`, + const resp = await fetch(`${getURLPrefix()}/api/${projectName}/${version}`, { method: 'POST', body @@ -104,7 +125,7 @@ async function upload (projectName: string, version: string, body: FormData): Pr * @param {string} projectName Name of the project */ async function claim (projectName: string): Promise<{ token: string }> { - const resp = await fetch(`/api/${projectName}/claim`) + const resp = await fetch(`${getURLPrefix()}/api/${projectName}/claim`) if (resp.ok) { const json = await resp.json() as { token: string } @@ -127,7 +148,7 @@ async function claim (projectName: string): Promise<{ token: string }> { */ async function deleteDoc (projectName: string, version: string, token: string): Promise { const headers = { 'Docat-Api-Key': token } - const resp = await fetch(`/api/${projectName}/${version}`, + const resp = await fetch(`${getURLPrefix()}/api/${projectName}/${version}`, { method: 'DELETE', headers @@ -199,18 +220,28 @@ function setFavorite (projectName: string, shouldBeFavorite: boolean): void { } } +/** + * Returns the prefix path for the API + * @returns {string} - prefix path + */ +function getURLPrefix (): string { + return process.env.REACT_APP_PREFIX_PATH ?? '' +} + const exp = { getVersions, filterHiddenVersions, search, getProjectLogoURL, getProjectDocsURL, + getDocPageURL, upload, claim, deleteDoc, compareVersions, isFavorite, - setFavorite + setFavorite, + getURLPrefix } export default exp diff --git a/web/src/setupProxy.js b/web/src/setupProxy.js index 415b81e45..87c5ebb05 100644 --- a/web/src/setupProxy.js +++ b/web/src/setupProxy.js @@ -3,9 +3,10 @@ const { createProxyMiddleware } = require('http-proxy-middleware') module.exports = function (app) { const backendPort = process.env.BACKEND_PORT || 5000 const backendHost = process.env.BACKEND_HOST || 'localhost' + const prefixPath = process.env.REACT_APP_PREFIX_PATH || '' app.use( - '/api', + `${prefixPath}/api`, createProxyMiddleware({ target: `http://${backendHost}:${backendPort}`, changeOrigin: true @@ -13,7 +14,7 @@ module.exports = function (app) { ) app.use( - '/doc', + `${prefixPath}/doc`, createProxyMiddleware({ target: `http://${backendHost}:${backendPort}`, changeOrigin: true diff --git a/web/src/tests/repositories/ProjectRepository.test.ts b/web/src/tests/repositories/ProjectRepository.test.ts index a28663182..35fb1d948 100644 --- a/web/src/tests/repositories/ProjectRepository.test.ts +++ b/web/src/tests/repositories/ProjectRepository.test.ts @@ -60,7 +60,7 @@ describe('get project logo url', () => { const result = ProjectRepository.getProjectLogoURL(projectName) - expect(result).toEqual(`/doc/${projectName}/logo`) + expect(result).toEqual(`${ProjectRepository.getURLPrefix()}/doc/${projectName}/logo`) }) }) @@ -71,7 +71,7 @@ describe('get project docs url', () => { const result = ProjectRepository.getProjectDocsURL(projectName, version) - expect(result).toEqual(`/doc/${projectName}/${version}/`) + expect(result).toEqual(`${ProjectRepository.getURLPrefix()}/doc/${projectName}/${version}/`) }) test('should return the correct url with path', () => { @@ -81,10 +81,86 @@ describe('get project docs url', () => { const result = ProjectRepository.getProjectDocsURL(projectName, version, path) - expect(result).toEqual(`/doc/${projectName}/${version}/${path}`) + expect(result).toEqual(`${ProjectRepository.getURLPrefix()}/doc/${projectName}/${version}/${path}`) }) }) +describe('get doc page url', () => { + test('should create the correct url for localhost', () => { + const currentURL = 'http://localhost:8080/#/test-project/latest' + const projectName = 'test-project' + const version = '1.0.0' + const path = 'index.html' + const hideControls = false + + const result = ProjectRepository.getDocPageURL(currentURL, projectName, version, path, hideControls) + + expect(result).toEqual('http://localhost:8080/#/test-project/1.0.0/index.html') + }) + + test('should create the correct url for a custom domain', () => { + const currentURL = 'http://testdomain.com/#/test-project/latest' + const projectName = 'test-project' + const version = '1.0.0' + const path = 'index.html' + const hideControls = false + + const result = ProjectRepository.getDocPageURL(currentURL, projectName, version, path, hideControls) + + expect(result).toEqual('http://testdomain.com/#/test-project/1.0.0/index.html') + } + ) + test('should work with a long path', () => { + const currentURL = 'http://localhost:8080/#/test-project/latest' + const projectName = 'test-project' + const version = '1.0.0' + const path = '/long/path/to/file.html' + const hideControls = false + + const result = ProjectRepository.getDocPageURL(currentURL, projectName, version, path, hideControls) + + expect(result).toEqual('http://localhost:8080/#/test-project/1.0.0/long/path/to/file.html') + } + ) + + test('should work with hide controls', () => { + const currentURL = 'http://localhost:8080/#/test-project/latest' + const projectName = 'test-project' + const version = '1.0.0' + const path = 'index.html' + const hideControls = true + + const result = ProjectRepository.getDocPageURL(currentURL, projectName, version, path, hideControls) + + expect(result).toEqual('http://localhost:8080/#/test-project/1.0.0/index.html?hide-ui=true') + }) + + test('should work with custom subpath', () => { + const currentURL = 'http://testdomain.com/#/test-project/latest' + const projectName = 'test-project' + const version = '1.0.0' + const path = 'index.html' + const hideControls = false + + const result = ProjectRepository.getDocPageURL(currentURL, projectName, version, path, hideControls) + + expect(result).toEqual('http://testdomain.com/#/test-project/1.0.0/index.html') + }) + + test('should work with more than one hashtag in the path', () => { + const currentURL = 'http://localhost:8080/#/test-project/latest' + const projectName = 'test-project' + const version = '1.0.0' + const path = '/file/index.html#section' + const hideControls = false + + const result = ProjectRepository.getDocPageURL(currentURL, projectName, version, path, hideControls) + + expect(result).toEqual('http://localhost:8080/#/test-project/1.0.0/file/index.html#section') + } + ) +}) + describe('upload', () => { test('should post file', async () => { const project = 'test-project' @@ -98,7 +174,7 @@ describe('upload', () => { await ProjectRepository.upload(project, version, body) expect(global.fetch).toHaveBeenCalledTimes(1) - expect(global.fetch).toHaveBeenCalledWith(`/api/${project}/${version}`, + expect(global.fetch).toHaveBeenCalledWith(`${ProjectRepository.getURLPrefix()}/api/${project}/${version}`, { body, method: 'POST' @@ -155,7 +231,7 @@ describe('claim project', () => { const respToken = await ProjectRepository.claim(project) expect(global.fetch).toHaveBeenCalledTimes(1) - expect(global.fetch).toHaveBeenCalledWith(`/api/${project}/claim`) + expect(global.fetch).toHaveBeenCalledWith(`${ProjectRepository.getURLPrefix()}/api/${project}/claim`) expect(respToken.token).toEqual('test-token') }) @@ -188,7 +264,7 @@ describe('deleteDoc', () => { await ProjectRepository.deleteDoc(project, version, token) expect(global.fetch).toHaveBeenCalledTimes(1) - expect(global.fetch).toHaveBeenCalledWith(`/api/${project}/${version}`, { method: 'DELETE', headers: { 'Docat-Api-Key': token } }) + expect(global.fetch).toHaveBeenCalledWith(`${ProjectRepository.getURLPrefix()}/api/${project}/${version}`, { method: 'DELETE', headers: { 'Docat-Api-Key': token } }) }) test('should throw invalid token on 401 status code', async () => {