Define base for assets and support for sub path (#1247)

* Added correct mime type to files

* Define Base for Assets

Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>

* lint

Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>

* Make things relative

Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>

* hop styling

Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Daniel Valdivia
2021-11-22 21:02:16 -08:00
committed by GitHub
parent 820fa61b43
commit 53d278a91e
11 changed files with 171 additions and 126 deletions

View File

@@ -185,7 +185,8 @@ func serveProxy(responseWriter http.ResponseWriter, req *http.Request) {
responseWriter.WriteHeader(500) responseWriter.WriteHeader(500)
return 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{ proxiedCookie := &http.Cookie{
Name: "token", Name: "token",
@@ -207,8 +208,17 @@ func serveProxy(responseWriter http.ResponseWriter, req *http.Request) {
CheckRedirect: func(req *http.Request, via []*http.Request) error { CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse 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 <base href="" /> 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 // copy query params
targetURL.RawQuery = req.URL.Query().Encode() targetURL.RawQuery = req.URL.Query().Encode()
proxRequest, err := http.NewRequest(req.Method, targetURL.String(), req.Body) proxRequest, err := http.NewRequest(req.Method, targetURL.String(), req.Body)
if err != nil { if err != nil {
log.Println(err) log.Println(err)

View File

@@ -2,51 +2,52 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <base href="/" />
<meta content="width=device-width, initial-scale=1" name="viewport" />
<meta <meta
name="theme-color"
content="#081C42" content="#081C42"
media="(prefers-color-scheme: light)" media="(prefers-color-scheme: light)"
name="theme-color"
/> />
<meta <meta
name="theme-color"
content="#081C42" content="#081C42"
media="(prefers-color-scheme: dark)" media="(prefers-color-scheme: dark)"
name="theme-color"
/> />
<meta name="description" content="MinIO Console" /> <meta content="MinIO Console" name="description" />
<!-- <!--
manifest.json provides metadata used when your web app is installed on a manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
--> -->
<link href="%PUBLIC_URL%/styles/root-styles.css" rel="stylesheet" /> <link href="%PUBLIC_URL%/styles/root-styles.css" rel="stylesheet" />
<link <link
href="%PUBLIC_URL%/apple-icon-180x180.png"
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="180x180" sizes="180x180"
href="%PUBLIC_URL%/apple-icon-180x180.png"
/> />
<link <link
rel="icon"
type="image/png"
sizes="32x32"
href="%PUBLIC_URL%/favicon-32x32.png" href="%PUBLIC_URL%/favicon-32x32.png"
rel="icon"
sizes="32x32"
type="image/png"
/> />
<link <link
rel="icon"
type="image/png"
sizes="96x96"
href="%PUBLIC_URL%/favicon-96x96.png" href="%PUBLIC_URL%/favicon-96x96.png"
/>
<link
rel="icon" rel="icon"
sizes="96x96"
type="image/png" type="image/png"
sizes="16x16"
href="%PUBLIC_URL%/favicon-16x16.png"
/> />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link <link
rel="mask-icon" href="%PUBLIC_URL%/favicon-16x16.png"
href="%PUBLIC_URL%/safari-pinned-tab.svg" rel="icon"
sizes="16x16"
type="image/png"
/>
<link href="%PUBLIC_URL%/manifest.json" rel="manifest" />
<link
color="#3a4e54" color="#3a4e54"
href="%PUBLIC_URL%/safari-pinned-tab.svg"
rel="mask-icon"
/> />
<!-- <!--
Notice the use of %PUBLIC_URL% in the tags above. Notice the use of %PUBLIC_URL% in the tags above.
@@ -71,22 +72,22 @@
" "
cx="44" cx="44"
cy="44" cy="44"
r="20.2"
fill="none" fill="none"
r="20.2"
stroke-width="3.6" stroke-width="3.6"
></circle> ></circle>
</svg> </svg>
</div> </div>
</div> </div>
<!-- <!--
This HTML file is a template. This HTML file is a template.
If you open it directly in the browser, you will see an empty page. If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file. You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag. The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`. To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`. To create a production bundle, use `npm run build` or `yarn build`.
--> -->
</body> </body>
</html> </html>

View File

@@ -20,8 +20,8 @@ import { connect } from "react-redux";
import { AppState } from "./store"; import { AppState } from "./store";
import { import {
consoleOperatorMode, consoleOperatorMode,
userLoggedIn,
setDistributedMode, setDistributedMode,
userLoggedIn,
} from "./actions"; } from "./actions";
import api from "./common/api"; import api from "./common/api";
import { saveSessionResponse } from "./screens/Console/actions"; import { saveSessionResponse } from "./screens/Console/actions";

View File

@@ -17,12 +17,14 @@
import request from "superagent"; import request from "superagent";
import get from "lodash/get"; import get from "lodash/get";
import { clearSession } from "../utils"; import { clearSession } from "../utils";
import { baseUrl } from "../../history";
import { ErrorResponseHandler } from "../types"; import { ErrorResponseHandler } from "../types";
export class API { export class API {
invoke(method: string, url: string, data?: object) { 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) return request(method, targetURL)
.send(data) .send(data)
.then((res) => res.body) .then((res) => res.body)

View File

@@ -3,13 +3,4 @@ import { BrowserHistoryBuildOptions } from "history/createBrowserHistory";
let browserHistoryOpts: BrowserHistoryBuildOptions = {}; 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); export default createBrowserHistory(browserHistoryOpts);

View File

@@ -66,7 +66,6 @@ import RewindEnable from "./RewindEnable";
import DeleteMultipleObjects from "./DeleteMultipleObjects"; import DeleteMultipleObjects from "./DeleteMultipleObjects";
import PreviewFileModal from "../Preview/PreviewFileModal"; import PreviewFileModal from "../Preview/PreviewFileModal";
import { baseUrl } from "../../../../../../history";
import ScreenTitle from "../../../../Common/ScreenTitle/ScreenTitle"; import ScreenTitle from "../../../../Common/ScreenTitle/ScreenTitle";
import AddFolderIcon from "../../../../../../icons/AddFolderIcon"; import AddFolderIcon from "../../../../../../icons/AddFolderIcon";
import HistoryIcon from "../../../../../../icons/HistoryIcon"; import HistoryIcon from "../../../../../../icons/HistoryIcon";
@@ -597,7 +596,7 @@ const ListObjects = ({
} }
e.preventDefault(); e.preventDefault();
let files = e.target.files; let files = e.target.files;
let uploadUrl = `${baseUrl}/api/v1/buckets/${bucketName}/objects/upload`; let uploadUrl = `api/v1/buckets/${bucketName}/objects/upload`;
if (encodedPath !== "") { if (encodedPath !== "") {
uploadUrl = `${uploadUrl}?prefix=${encodedPath}`; uploadUrl = `${uploadUrl}?prefix=${encodedPath}`;
} }

View File

@@ -63,7 +63,7 @@ const PageHeader = ({
return ( return (
<Grid <Grid
container container
className={classes.headerContainer} className={`${classes.headerContainer} page-header`}
direction="row" direction="row"
alignItems="center" alignItems="center"
> >

View File

@@ -95,9 +95,7 @@ const Dashboard = React.lazy(() => import("./Dashboard/Dashboard"));
const Account = React.lazy(() => import("./Account/Account")); const Account = React.lazy(() => import("./Account/Account"));
const Users = React.lazy(() => import("./Users/Users")); const Users = React.lazy(() => import("./Users/Users"));
const Groups = React.lazy(() => import("./Groups/Groups")); const Groups = React.lazy(() => import("./Groups/Groups"));
const ConfigurationMain = React.lazy(
() => import("./Configurations/ConfigurationMain")
);
const TenantDetails = React.lazy( const TenantDetails = React.lazy(
() => import("./Tenants/TenantDetails/TenantDetails") () => import("./Tenants/TenantDetails/TenantDetails")
); );

View File

@@ -461,7 +461,7 @@ const Menu = ({
component: NavLink, component: NavLink,
to: "/license", to: "/license",
name: "License", name: "License",
icon: <LicenseIcon />, icon: LicenseIcon,
}, },
{ {
...documentation, ...documentation,

View File

@@ -19,7 +19,7 @@ import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles"; import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles"; import withStyles from "@mui/styles/withStyles";
import { Link } from "react-router-dom"; 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 PageHeader from "../../../Common/PageHeader/PageHeader";
import { containerForHeader } from "../../../Common/FormComponents/common/styleLibrary"; import { containerForHeader } from "../../../Common/FormComponents/common/styleLibrary";
import ExitToAppIcon from "@mui/icons-material/ExitToApp"; import ExitToAppIcon from "@mui/icons-material/ExitToApp";
@@ -46,8 +46,8 @@ const styles = (theme: Theme) =>
divContainer: { divContainer: {
position: "absolute", position: "absolute",
left: 0, left: 0,
top: 77, top: 80,
height: "calc(100vh - 77px)", height: "calc(100vh - 81px)",
width: "100%", width: "100%",
}, },
loader: { loader: {
@@ -55,6 +55,11 @@ const styles = (theme: Theme) =>
margin: "auto", margin: "auto",
marginTop: 80, marginTop: 80,
}, },
pageHeader: {
borderBottom: "1px solid #dedede",
},
...containerForHeader(theme.spacing(4)), ...containerForHeader(theme.spacing(4)),
}); });
@@ -66,72 +71,76 @@ const Hop = ({ classes, match }: IHopSimple) => {
const consoleFrame = React.useRef<HTMLIFrameElement>(null); const consoleFrame = React.useRef<HTMLIFrameElement>(null);
return ( return (
<React.Fragment> <Fragment>
<PageHeader <Box className={classes.pageHeader}>
label={ <PageHeader
<Fragment> label={
<Link to={"/tenants"} className={classes.breadcrumLink}> <Fragment>
Tenants <Link to={"/tenants"} className={classes.breadcrumLink}>
</Link> Tenants
{` > `} </Link>
<Link {` > `}
to={`/namespaces/${tenantNamespace}/tenants/${tenantName}`} <Link
className={classes.breadcrumLink} to={`/namespaces/${tenantNamespace}/tenants/${tenantName}`}
> className={classes.breadcrumLink}
{match.params["tenantName"]} >
</Link> {match.params["tenantName"]}
{` > Management`} </Link>
</Fragment> {` > Management`}
} </Fragment>
actions={ }
<React.Fragment> actions={
<IconButton <React.Fragment>
color="primary" <IconButton
aria-label="Refresh List" color="primary"
component="span" aria-label="Refresh List"
onClick={() => { component="span"
if ( onClick={() => {
consoleFrame !== null && if (
consoleFrame.current !== null && consoleFrame !== null &&
consoleFrame.current.contentDocument !== null consoleFrame.current !== null &&
) { consoleFrame.current.contentDocument !== null
const loc = ) {
consoleFrame.current.contentDocument.location.toString(); const loc =
consoleFrame.current.contentDocument.location.toString();
let add = "&"; let add = "&";
if (loc.indexOf("?") < 0) { if (loc.indexOf("?") < 0) {
add = `?`; 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) { size="large"
const next = `${loc}${add}cp=y`; >
consoleFrame.current.contentDocument.location.replace(next); <RefreshIcon />
} else { </IconButton>
consoleFrame.current.contentDocument.location.reload(); <IconButton
} color="primary"
} aria-label="Refresh List"
}} component="span"
size="large" onClick={() => {
> history.push(
<RefreshIcon /> `/namespaces/${tenantNamespace}/tenants/${tenantName}`
</IconButton> );
<IconButton }}
color="primary" size="large"
aria-label="Refresh List" >
component="span" <ExitToAppIcon />
onClick={() => { </IconButton>
history.push( </React.Fragment>
`/namespaces/${tenantNamespace}/tenants/${tenantName}` }
); />
}} </Box>
size="large"
>
<ExitToAppIcon />
</IconButton>
</React.Fragment>
}
/>
<div className={classes.divContainer}> <div className={classes.divContainer}>
{loading && ( {loading && (
<div className={classes.loader}> <div className={classes.loader}>
@@ -148,7 +157,7 @@ const Hop = ({ classes, match }: IHopSimple) => {
}} }}
/> />
</div> </div>
</React.Fragment> </Fragment>
); );
}; };

View File

@@ -21,14 +21,16 @@ package restapi
import ( import (
"bytes" "bytes"
"crypto/tls" "crypto/tls"
"fmt"
"io" "io"
"io/fs" "io/fs"
"log" "log"
"net" "net"
"net/http" "net/http"
"os"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"sync"
"time" "time"
"github.com/klauspost/compress/gzhttp" "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"` 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) { func configureFlags(api *operations.ConsoleAPI) {
api.CommandLineOptionsGroups = []swag.CommandLineOptionsGroup{ 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 { type notFoundRedirectRespWr struct {
http.ResponseWriter // We embed http.ResponseWriter http.ResponseWriter // We embed http.ResponseWriter
status int status int
@@ -274,9 +277,15 @@ func (w *notFoundRedirectRespWr) Write(p []byte) (int, error) {
func handleSPA(w http.ResponseWriter, r *http.Request) { func handleSPA(w http.ResponseWriter, r *http.Request) {
basePath := "/" 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" { 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") indexPage, err := portal_ui.GetStaticAssets().Open("build/index.html")
@@ -291,16 +300,21 @@ func handleSPA(w http.ResponseWriter, r *http.Request) {
return return
} }
if basePath != "./" { // if we have a seeded basePath. This should override CONSOLE_SUBPATH every time, thus the `if else`
indexPageStr := string(indexPageBytes) if basePath != "/" {
for _, match := range reHrefIndex.FindAllStringSubmatch(indexPageStr, -1) { indexPageBytes = replaceBaseInIndex(indexPageBytes, basePath)
toReplace := strings.Replace(match[1], match[3], basePath, 1) // if we have a custom subpath replace it in
indexPageStr = strings.Replace(indexPageStr, match[1], toReplace, 1) } else if getSubPath() != "/" {
} indexPageBytes = replaceBaseInIndex(indexPageBytes, getSubPath())
indexPageBytes = []byte(indexPageStr)
} }
w.Header().Set("Content-Type", "text/html; charset=utf-8") mimeType := mimedb.TypeByExtension(filepath.Ext(r.URL.Path))
if mimeType == "application/octet-stream" {
mimeType = "text/html"
}
w.Header().Set("Content-Type", mimeType)
http.ServeContent(w, r, "index.html", time.Now(), bytes.NewReader(indexPageBytes)) 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 // Turn-off random logging by Go net/http
s.ErrorLog = log.New(&nullWriter{}, "", 0) 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("<base href=\"%s\"/>", basePath)
indexPageStr = strings.Replace(indexPageStr, "<base href=\"/\"/>", newBase, 1)
indexPageBytes = []byte(indexPageStr)
return indexPageBytes
}