diff --git a/operatorapi/proxy.go b/operatorapi/proxy.go index 51c7f89f7d..835a9dc897 100644 --- a/operatorapi/proxy.go +++ b/operatorapi/proxy.go @@ -185,7 +185,8 @@ func serveProxy(responseWriter http.ResponseWriter, req *http.Request) { responseWriter.WriteHeader(500) return } - targetURL.Path = strings.Replace(req.URL.Path, fmt.Sprintf("/api/proxy/%s/%s", tenant.Namespace, tenant.Name), "", -1) + tenantBase := fmt.Sprintf("/api/proxy/%s/%s", tenant.Namespace, tenant.Name) + targetURL.Path = strings.Replace(req.URL.Path, tenantBase, "", -1) proxiedCookie := &http.Cookie{ Name: "token", @@ -207,8 +208,17 @@ func serveProxy(responseWriter http.ResponseWriter, req *http.Request) { CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }} + + // are we proxying something with cp=y? (console proxy) then add cpb (console proxy base) so the console + // on the other side updates the to this value overriding sub path or root + if v := req.URL.Query().Get("cp"); v == "y" { + q := req.URL.Query() + q.Add("cpb", tenantBase) + req.URL.RawQuery = q.Encode() + } // copy query params targetURL.RawQuery = req.URL.Query().Encode() + proxRequest, err := http.NewRequest(req.Method, targetURL.String(), req.Body) if err != nil { log.Println(err) diff --git a/portal-ui/public/index.html b/portal-ui/public/index.html index 0466c35787..564a2d7d57 100644 --- a/portal-ui/public/index.html +++ b/portal-ui/public/index.html @@ -2,51 +2,52 @@ - + + - + - + +To begin the development, run `npm start` or `yarn start`. +To create a production bundle, use `npm run build` or `yarn build`. +--> diff --git a/portal-ui/src/ProtectedRoutes.tsx b/portal-ui/src/ProtectedRoutes.tsx index 2028cbf010..5f168f9ebf 100644 --- a/portal-ui/src/ProtectedRoutes.tsx +++ b/portal-ui/src/ProtectedRoutes.tsx @@ -20,8 +20,8 @@ import { connect } from "react-redux"; import { AppState } from "./store"; import { consoleOperatorMode, - userLoggedIn, setDistributedMode, + userLoggedIn, } from "./actions"; import api from "./common/api"; import { saveSessionResponse } from "./screens/Console/actions"; diff --git a/portal-ui/src/common/api/index.ts b/portal-ui/src/common/api/index.ts index 1a56fd522f..c712dd0745 100644 --- a/portal-ui/src/common/api/index.ts +++ b/portal-ui/src/common/api/index.ts @@ -17,12 +17,14 @@ import request from "superagent"; import get from "lodash/get"; import { clearSession } from "../utils"; -import { baseUrl } from "../../history"; import { ErrorResponseHandler } from "../types"; export class API { invoke(method: string, url: string, data?: object) { - const targetURL = `${baseUrl}${url}`.replace(/\/\//g, "/"); + let targetURL = url; + if (targetURL[0] === "/") { + targetURL = targetURL.substr(1); + } return request(method, targetURL) .send(data) .then((res) => res.body) diff --git a/portal-ui/src/history.ts b/portal-ui/src/history.ts index e56e6ed188..9ecc96453b 100644 --- a/portal-ui/src/history.ts +++ b/portal-ui/src/history.ts @@ -3,13 +3,4 @@ import { BrowserHistoryBuildOptions } from "history/createBrowserHistory"; let browserHistoryOpts: BrowserHistoryBuildOptions = {}; -export let baseUrl = ""; - -if (`${window.location.pathname}`.startsWith("/api/proxy/")) { - // grab from api to the tenant name (/api/proxy/namespace/tenant) - const urlParts = `${window.location.pathname}`.split("/").slice(0, 5); - browserHistoryOpts.basename = urlParts.join("/"); - baseUrl = `${urlParts.join("/")}/`; -} - export default createBrowserHistory(browserHistoryOpts); diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx index e8abb00caa..3a8dcb02f6 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx @@ -66,7 +66,6 @@ import RewindEnable from "./RewindEnable"; import DeleteMultipleObjects from "./DeleteMultipleObjects"; import PreviewFileModal from "../Preview/PreviewFileModal"; -import { baseUrl } from "../../../../../../history"; import ScreenTitle from "../../../../Common/ScreenTitle/ScreenTitle"; import AddFolderIcon from "../../../../../../icons/AddFolderIcon"; import HistoryIcon from "../../../../../../icons/HistoryIcon"; @@ -597,7 +596,7 @@ const ListObjects = ({ } e.preventDefault(); let files = e.target.files; - let uploadUrl = `${baseUrl}/api/v1/buckets/${bucketName}/objects/upload`; + let uploadUrl = `api/v1/buckets/${bucketName}/objects/upload`; if (encodedPath !== "") { uploadUrl = `${uploadUrl}?prefix=${encodedPath}`; } diff --git a/portal-ui/src/screens/Console/Common/PageHeader/PageHeader.tsx b/portal-ui/src/screens/Console/Common/PageHeader/PageHeader.tsx index 978dce9793..24de1191b9 100644 --- a/portal-ui/src/screens/Console/Common/PageHeader/PageHeader.tsx +++ b/portal-ui/src/screens/Console/Common/PageHeader/PageHeader.tsx @@ -63,7 +63,7 @@ const PageHeader = ({ return ( diff --git a/portal-ui/src/screens/Console/Console.tsx b/portal-ui/src/screens/Console/Console.tsx index 01869da79f..ed18668d6c 100644 --- a/portal-ui/src/screens/Console/Console.tsx +++ b/portal-ui/src/screens/Console/Console.tsx @@ -95,9 +95,7 @@ const Dashboard = React.lazy(() => import("./Dashboard/Dashboard")); const Account = React.lazy(() => import("./Account/Account")); const Users = React.lazy(() => import("./Users/Users")); const Groups = React.lazy(() => import("./Groups/Groups")); -const ConfigurationMain = React.lazy( - () => import("./Configurations/ConfigurationMain") -); + const TenantDetails = React.lazy( () => import("./Tenants/TenantDetails/TenantDetails") ); diff --git a/portal-ui/src/screens/Console/Menu/Menu.tsx b/portal-ui/src/screens/Console/Menu/Menu.tsx index 1e18d7f3da..bf07cca835 100644 --- a/portal-ui/src/screens/Console/Menu/Menu.tsx +++ b/portal-ui/src/screens/Console/Menu/Menu.tsx @@ -461,7 +461,7 @@ const Menu = ({ component: NavLink, to: "/license", name: "License", - icon: , + icon: LicenseIcon, }, { ...documentation, diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/hop/Hop.tsx b/portal-ui/src/screens/Console/Tenants/TenantDetails/hop/Hop.tsx index f3eed94b92..d1f110ca59 100644 --- a/portal-ui/src/screens/Console/Tenants/TenantDetails/hop/Hop.tsx +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/hop/Hop.tsx @@ -19,7 +19,7 @@ import { Theme } from "@mui/material/styles"; import createStyles from "@mui/styles/createStyles"; import withStyles from "@mui/styles/withStyles"; import { Link } from "react-router-dom"; -import { CircularProgress, IconButton } from "@mui/material"; +import { Box, CircularProgress, IconButton } from "@mui/material"; import PageHeader from "../../../Common/PageHeader/PageHeader"; import { containerForHeader } from "../../../Common/FormComponents/common/styleLibrary"; import ExitToAppIcon from "@mui/icons-material/ExitToApp"; @@ -46,8 +46,8 @@ const styles = (theme: Theme) => divContainer: { position: "absolute", left: 0, - top: 77, - height: "calc(100vh - 77px)", + top: 80, + height: "calc(100vh - 81px)", width: "100%", }, loader: { @@ -55,6 +55,11 @@ const styles = (theme: Theme) => margin: "auto", marginTop: 80, }, + + pageHeader: { + borderBottom: "1px solid #dedede", + }, + ...containerForHeader(theme.spacing(4)), }); @@ -66,72 +71,76 @@ const Hop = ({ classes, match }: IHopSimple) => { const consoleFrame = React.useRef(null); return ( - - - - Tenants - - {` > `} - - {match.params["tenantName"]} - - {` > Management`} - - } - actions={ - - { - if ( - consoleFrame !== null && - consoleFrame.current !== null && - consoleFrame.current.contentDocument !== null - ) { - const loc = - consoleFrame.current.contentDocument.location.toString(); + + + + + Tenants + + {` > `} + + {match.params["tenantName"]} + + {` > Management`} + + } + actions={ + + { + if ( + consoleFrame !== null && + consoleFrame.current !== null && + consoleFrame.current.contentDocument !== null + ) { + const loc = + consoleFrame.current.contentDocument.location.toString(); - let add = "&"; + let add = "&"; - if (loc.indexOf("?") < 0) { - add = `?`; - } + if (loc.indexOf("?") < 0) { + add = `?`; + } - if (loc.indexOf("cp=y") < 0) { - const next = `${loc}${add}cp=y`; - consoleFrame.current.contentDocument.location.replace(next); - } else { - consoleFrame.current.contentDocument.location.reload(); + if (loc.indexOf("cp=y") < 0) { + const next = `${loc}${add}cp=y`; + consoleFrame.current.contentDocument.location.replace( + next + ); + } else { + consoleFrame.current.contentDocument.location.reload(); + } } - } - }} - size="large" - > - - - { - history.push( - `/namespaces/${tenantNamespace}/tenants/${tenantName}` - ); - }} - size="large" - > - - - - } - /> + }} + size="large" + > + + + { + history.push( + `/namespaces/${tenantNamespace}/tenants/${tenantName}` + ); + }} + size="large" + > + + + + } + /> +
{loading && (
@@ -148,7 +157,7 @@ const Hop = ({ classes, match }: IHopSimple) => { }} />
- + ); }; diff --git a/restapi/configure_console.go b/restapi/configure_console.go index b33ef6afcb..2d6caa79fb 100644 --- a/restapi/configure_console.go +++ b/restapi/configure_console.go @@ -21,14 +21,16 @@ package restapi import ( "bytes" "crypto/tls" + "fmt" "io" "io/fs" "log" "net" "net/http" + "os" "path/filepath" - "regexp" "strings" + "sync" "time" "github.com/klauspost/compress/gzhttp" @@ -50,6 +52,9 @@ var additionalServerFlags = struct { CertsDir string `long:"certs-dir" description:"path to certs directory" env:"CONSOLE_CERTS_DIR"` }{} +var subPath = "/" +var subPathOnce sync.Once + func configureFlags(api *operations.ConsoleAPI) { api.CommandLineOptionsGroups = []swag.CommandLineOptionsGroup{ { @@ -251,8 +256,6 @@ func FileServerMiddleware(next http.Handler) http.Handler { }) } -var reHrefIndex = regexp.MustCompile(`(?m)((href|src)="(.\/).*?")`) - type notFoundRedirectRespWr struct { http.ResponseWriter // We embed http.ResponseWriter status int @@ -274,9 +277,15 @@ func (w *notFoundRedirectRespWr) Write(p []byte) (int, error) { func handleSPA(w http.ResponseWriter, r *http.Request) { basePath := "/" - // For SPA mode we will replace relative paths with absolute unless we receive query param cp=y + // For SPA mode we will replace root base with a sub path if configured unless we received cp=y and cpb=/NEW/BASE if v := r.URL.Query().Get("cp"); v == "y" { - basePath = "./" + if base := r.URL.Query().Get("cpb"); base != "" { + // make sure the subpath has a trailing slash + if !strings.HasSuffix(base, "/") { + base = fmt.Sprintf("%s/", base) + } + basePath = base + } } indexPage, err := portal_ui.GetStaticAssets().Open("build/index.html") @@ -291,16 +300,21 @@ func handleSPA(w http.ResponseWriter, r *http.Request) { return } - if basePath != "./" { - indexPageStr := string(indexPageBytes) - for _, match := range reHrefIndex.FindAllStringSubmatch(indexPageStr, -1) { - toReplace := strings.Replace(match[1], match[3], basePath, 1) - indexPageStr = strings.Replace(indexPageStr, match[1], toReplace, 1) - } - indexPageBytes = []byte(indexPageStr) + // if we have a seeded basePath. This should override CONSOLE_SUBPATH every time, thus the `if else` + if basePath != "/" { + indexPageBytes = replaceBaseInIndex(indexPageBytes, basePath) + // if we have a custom subpath replace it in + } else if getSubPath() != "/" { + indexPageBytes = replaceBaseInIndex(indexPageBytes, getSubPath()) + } + + mimeType := mimedb.TypeByExtension(filepath.Ext(r.URL.Path)) + + if mimeType == "application/octet-stream" { + mimeType = "text/html" } - w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Header().Set("Content-Type", mimeType) http.ServeContent(w, r, "index.html", time.Now(), bytes.NewReader(indexPageBytes)) } @@ -335,3 +349,24 @@ func configureServer(s *http.Server, _, _ string) { // Turn-off random logging by Go net/http s.ErrorLog = log.New(&nullWriter{}, "", 0) } + +func getSubPath() string { + subPathOnce.Do(func() { + if v := os.Getenv("CONSOLE_SUBPATH"); v != "" { + // make sure the subpath has a trailing slash + if !strings.HasSuffix(v, "/") { + v = fmt.Sprintf("%s/", v) + } + subPath = v + } + }) + return subPath +} + +func replaceBaseInIndex(indexPageBytes []byte, basePath string) []byte { + indexPageStr := string(indexPageBytes) + newBase := fmt.Sprintf("", basePath) + indexPageStr = strings.Replace(indexPageStr, "", newBase, 1) + indexPageBytes = []byte(indexPageStr) + return indexPageBytes +}