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)
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 <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
targetURL.RawQuery = req.URL.Query().Encode()
proxRequest, err := http.NewRequest(req.Method, targetURL.String(), req.Body)
if err != nil {
log.Println(err)

View File

@@ -2,51 +2,52 @@
<html lang="en">
<head>
<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
name="theme-color"
content="#081C42"
media="(prefers-color-scheme: light)"
name="theme-color"
/>
<meta
name="theme-color"
content="#081C42"
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
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%/apple-icon-180x180.png"
rel="apple-touch-icon"
sizes="180x180"
href="%PUBLIC_URL%/apple-icon-180x180.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="%PUBLIC_URL%/favicon-32x32.png"
rel="icon"
sizes="32x32"
type="image/png"
/>
<link
rel="icon"
type="image/png"
sizes="96x96"
href="%PUBLIC_URL%/favicon-96x96.png"
/>
<link
rel="icon"
sizes="96x96"
type="image/png"
sizes="16x16"
href="%PUBLIC_URL%/favicon-16x16.png"
/>
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link
rel="mask-icon"
href="%PUBLIC_URL%/safari-pinned-tab.svg"
href="%PUBLIC_URL%/favicon-16x16.png"
rel="icon"
sizes="16x16"
type="image/png"
/>
<link href="%PUBLIC_URL%/manifest.json" rel="manifest" />
<link
color="#3a4e54"
href="%PUBLIC_URL%/safari-pinned-tab.svg"
rel="mask-icon"
/>
<!--
Notice the use of %PUBLIC_URL% in the tags above.
@@ -71,22 +72,22 @@
"
cx="44"
cy="44"
r="20.2"
fill="none"
r="20.2"
stroke-width="3.6"
></circle>
</svg>
</div>
</div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
This HTML file is a template.
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.
The build step will place the bundled scripts into the <body> tag.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@@ -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";

View File

@@ -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)

View File

@@ -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);

View File

@@ -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}`;
}

View File

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

View File

@@ -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")
);

View File

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

View File

@@ -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<HTMLIFrameElement>(null);
return (
<React.Fragment>
<PageHeader
label={
<Fragment>
<Link to={"/tenants"} className={classes.breadcrumLink}>
Tenants
</Link>
{` > `}
<Link
to={`/namespaces/${tenantNamespace}/tenants/${tenantName}`}
className={classes.breadcrumLink}
>
{match.params["tenantName"]}
</Link>
{` > Management`}
</Fragment>
}
actions={
<React.Fragment>
<IconButton
color="primary"
aria-label="Refresh List"
component="span"
onClick={() => {
if (
consoleFrame !== null &&
consoleFrame.current !== null &&
consoleFrame.current.contentDocument !== null
) {
const loc =
consoleFrame.current.contentDocument.location.toString();
<Fragment>
<Box className={classes.pageHeader}>
<PageHeader
label={
<Fragment>
<Link to={"/tenants"} className={classes.breadcrumLink}>
Tenants
</Link>
{` > `}
<Link
to={`/namespaces/${tenantNamespace}/tenants/${tenantName}`}
className={classes.breadcrumLink}
>
{match.params["tenantName"]}
</Link>
{` > Management`}
</Fragment>
}
actions={
<React.Fragment>
<IconButton
color="primary"
aria-label="Refresh List"
component="span"
onClick={() => {
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"
>
<RefreshIcon />
</IconButton>
<IconButton
color="primary"
aria-label="Refresh List"
component="span"
onClick={() => {
history.push(
`/namespaces/${tenantNamespace}/tenants/${tenantName}`
);
}}
size="large"
>
<ExitToAppIcon />
</IconButton>
</React.Fragment>
}
/>
}}
size="large"
>
<RefreshIcon />
</IconButton>
<IconButton
color="primary"
aria-label="Refresh List"
component="span"
onClick={() => {
history.push(
`/namespaces/${tenantNamespace}/tenants/${tenantName}`
);
}}
size="large"
>
<ExitToAppIcon />
</IconButton>
</React.Fragment>
}
/>
</Box>
<div className={classes.divContainer}>
{loading && (
<div className={classes.loader}>
@@ -148,7 +157,7 @@ const Hop = ({ classes, match }: IHopSimple) => {
}}
/>
</div>
</React.Fragment>
</Fragment>
);
};

View File

@@ -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())
}
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))
}
@@ -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("<base href=\"%s\"/>", basePath)
indexPageStr = strings.Replace(indexPageStr, "<base href=\"/\"/>", newBase, 1)
indexPageBytes = []byte(indexPageStr)
return indexPageBytes
}