Added Color customization to embedded object browser (#2246)
Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
@@ -46,6 +46,9 @@ type Principal struct {
|
||||
// account access key
|
||||
AccountAccessKey string `json:"accountAccessKey,omitempty"`
|
||||
|
||||
// custom style ob
|
||||
CustomStyleOb string `json:"customStyleOb,omitempty"`
|
||||
|
||||
// hm
|
||||
Hm bool `json:"hm,omitempty"`
|
||||
|
||||
|
||||
@@ -41,6 +41,9 @@ type SessionResponse struct {
|
||||
// allow resources
|
||||
AllowResources []*PermissionResource `json:"allowResources"`
|
||||
|
||||
// custom styles
|
||||
CustomStyles string `json:"customStyles,omitempty"`
|
||||
|
||||
// distributed mode
|
||||
DistributedMode bool `json:"distributedMode,omitempty"`
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ type TokenClaims struct {
|
||||
AccountAccessKey string `json:"accountAccessKey,omitempty"`
|
||||
HideMenu bool `json:"hm,omitempty"`
|
||||
ObjectBrowser bool `json:"ob,omitempty"`
|
||||
CustomStyleOB string `json:"customStyleOb,omitempty"`
|
||||
}
|
||||
|
||||
// STSClaims claims struct for STS Token
|
||||
@@ -79,6 +80,7 @@ type STSClaims struct {
|
||||
type SessionFeatures struct {
|
||||
HideMenu bool
|
||||
ObjectBrowser bool
|
||||
CustomStyleOB string
|
||||
}
|
||||
|
||||
// SessionTokenAuthenticate takes a session token, decode it, extract claims and validate the signature
|
||||
@@ -123,7 +125,9 @@ func NewEncryptedTokenForClient(credentials *credentials.Value, accountAccessKey
|
||||
if features != nil {
|
||||
tokenClaims.HideMenu = features.HideMenu
|
||||
tokenClaims.ObjectBrowser = features.ObjectBrowser
|
||||
tokenClaims.CustomStyleOB = features.CustomStyleOB
|
||||
}
|
||||
|
||||
encryptedClaims, err := encryptClaims(tokenClaims)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"react-window-infinite-loader": "^1.0.7",
|
||||
"recharts": "^2.1.1",
|
||||
"superagent": "^6.1.0",
|
||||
"tinycolor2": "^1.4.2",
|
||||
"websocket": "^1.0.31"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -27,12 +27,14 @@ import {
|
||||
globalSetDistributedSetup,
|
||||
operatorMode,
|
||||
selOpMode,
|
||||
setOverrideStyles,
|
||||
setSiteReplicationInfo,
|
||||
userLogged,
|
||||
} from "./systemSlice";
|
||||
import { SRInfoStateType } from "./types";
|
||||
import { AppState, useAppDispatch } from "./store";
|
||||
import { saveSessionResponse } from "./screens/Console/consoleSlice";
|
||||
import { getOverrideColorVariants } from "./utils/stylesUtils";
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
Component: any;
|
||||
@@ -67,6 +69,16 @@ const ProtectedRoute = ({ Component }: ProtectedRouteProps) => {
|
||||
dispatch(directPVMode(!!res.directPV));
|
||||
document.title = "MinIO Operator";
|
||||
}
|
||||
|
||||
if (res.customStyles && res.customStyles !== "") {
|
||||
const overrideColorVariants = getOverrideColorVariants(
|
||||
res.customStyles
|
||||
);
|
||||
|
||||
if (overrideColorVariants !== false) {
|
||||
dispatch(setOverrideStyles(overrideColorVariants));
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => setSessionLoading(false));
|
||||
}, [dispatch]);
|
||||
|
||||
168
portal-ui/src/StyleHandler.tsx
Normal file
168
portal-ui/src/StyleHandler.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2021 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { Fragment } from "react";
|
||||
import {
|
||||
StyledEngineProvider,
|
||||
Theme,
|
||||
ThemeProvider,
|
||||
} from "@mui/material/styles";
|
||||
import withStyles from "@mui/styles/withStyles";
|
||||
import theme from "./theme/main";
|
||||
import "react-virtualized/styles.css";
|
||||
import "react-grid-layout/css/styles.css";
|
||||
import "react-resizable/css/styles.css";
|
||||
|
||||
import { generateOverrideTheme } from "./utils/stylesUtils";
|
||||
import "./index.css";
|
||||
import { useSelector } from "react-redux";
|
||||
import { AppState } from "./store";
|
||||
|
||||
declare module "@mui/styles/defaultTheme" {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface DefaultTheme extends Theme {}
|
||||
}
|
||||
|
||||
interface IStyleHandler {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const StyleHandler = ({ children }: IStyleHandler) => {
|
||||
const colorVariants = useSelector(
|
||||
(state: AppState) => state.system.overrideStyles
|
||||
);
|
||||
|
||||
let thm = theme;
|
||||
let globalBody: any = {};
|
||||
let rowColor: any = { color: "#393939" };
|
||||
let detailsListPanel: any = { backgroundColor: "#fff" };
|
||||
|
||||
if (colorVariants) {
|
||||
thm = generateOverrideTheme(colorVariants);
|
||||
|
||||
globalBody = { backgroundColor: colorVariants.backgroundColor };
|
||||
rowColor = { color: colorVariants.fontColor };
|
||||
detailsListPanel = {
|
||||
backgroundColor: colorVariants.backgroundColor,
|
||||
color: colorVariants.fontColor,
|
||||
};
|
||||
}
|
||||
|
||||
const GlobalCss = withStyles({
|
||||
// @global is handled by jss-plugin-global.
|
||||
"@global": {
|
||||
body: {
|
||||
height: "100vh",
|
||||
width: "100vw",
|
||||
fontFamily: "Lato, sans-serif",
|
||||
...globalBody,
|
||||
},
|
||||
"#root": {
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
alignItems: "stretch",
|
||||
},
|
||||
".min-icon": {
|
||||
width: 26,
|
||||
},
|
||||
".MuiButton-endIcon": {
|
||||
"& .min-icon": {
|
||||
width: 16,
|
||||
},
|
||||
},
|
||||
// You should target [class*="MuiButton-root"] instead if you nest themes.
|
||||
".MuiButton-root:not(.noDefaultHeight)": {
|
||||
height: 38,
|
||||
},
|
||||
".MuiButton-contained": {
|
||||
fontSize: "14px",
|
||||
textTransform: "capitalize",
|
||||
padding: "15px 25px 15px 25px",
|
||||
borderRadius: 3,
|
||||
},
|
||||
".MuiButton-sizeSmall": {
|
||||
padding: "4px 10px",
|
||||
fontSize: "0.8125rem",
|
||||
},
|
||||
".MuiTableCell-head": {
|
||||
borderRadius: "3px 3px 0px 0px",
|
||||
fontSize: 13,
|
||||
},
|
||||
".MuiPaper-root": {
|
||||
borderRadius: 3,
|
||||
},
|
||||
".MuiDrawer-paperAnchorDockedLeft": {
|
||||
borderRight: 0,
|
||||
},
|
||||
".MuiDrawer-root": {
|
||||
"& .MuiPaper-root": {
|
||||
borderRadius: 0,
|
||||
},
|
||||
},
|
||||
".rowLine": {
|
||||
...rowColor,
|
||||
},
|
||||
".detailsListPanel": {
|
||||
...detailsListPanel,
|
||||
},
|
||||
hr: {
|
||||
borderTop: 0,
|
||||
borderLeft: 0,
|
||||
borderRight: 0,
|
||||
borderColor: "#999999",
|
||||
backgroundColor: "transparent" as const,
|
||||
},
|
||||
ul: {
|
||||
paddingLeft: 20,
|
||||
listStyle: "none" /* Remove default bullets */,
|
||||
"& li::before:not(.Mui*)": {
|
||||
content: '"■"',
|
||||
color: "#2781B0",
|
||||
fontSize: 20,
|
||||
display:
|
||||
"inline-block" /* Needed to add space between the bullet and the text */,
|
||||
width: "1em" /* Also needed for space (tweak if needed) */,
|
||||
marginLeft: "-1em" /* Also needed for space (tweak if needed) */,
|
||||
},
|
||||
"& ul": {
|
||||
listStyle: "none" /* Remove default bullets */,
|
||||
"& li::before:not(.Mui*)": {
|
||||
content: '"○"',
|
||||
color: "#2781B0",
|
||||
fontSize: 20,
|
||||
display:
|
||||
"inline-block" /* Needed to add space between the bullet and the text */,
|
||||
width: "1em" /* Also needed for space (tweak if needed) */,
|
||||
marginLeft: "-1em" /* Also needed for space (tweak if needed) */,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})(() => null);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<GlobalCss />
|
||||
<StyledEngineProvider injectFirst>
|
||||
<ThemeProvider theme={thm}>{children}</ThemeProvider>
|
||||
</StyledEngineProvider>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default StyleHandler;
|
||||
@@ -462,3 +462,18 @@ export interface IBytesCalc {
|
||||
total: number;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
export interface IEmbeddedCustomButton {
|
||||
backgroundColor?: string;
|
||||
textColor?: string;
|
||||
hoverColor?: string;
|
||||
hoverText?: string;
|
||||
activeColor?: string;
|
||||
activeText?: string;
|
||||
}
|
||||
|
||||
export interface IEmbeddedCustomStyles {
|
||||
backgroundColor: string;
|
||||
fontColor: string;
|
||||
buttonStyles: IEmbeddedCustomButton;
|
||||
}
|
||||
|
||||
@@ -15,117 +15,12 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React from "react";
|
||||
import * as serviceWorker from "./serviceWorker";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { Provider } from "react-redux";
|
||||
import { store } from "./store";
|
||||
import * as serviceWorker from "./serviceWorker";
|
||||
import {
|
||||
StyledEngineProvider,
|
||||
Theme,
|
||||
ThemeProvider,
|
||||
} from "@mui/material/styles";
|
||||
import withStyles from "@mui/styles/withStyles";
|
||||
import "react-virtualized/styles.css";
|
||||
import "react-grid-layout/css/styles.css";
|
||||
import "react-resizable/css/styles.css";
|
||||
|
||||
import "./index.css";
|
||||
import theme from "./theme/main";
|
||||
import MainRouter from "./MainRouter";
|
||||
|
||||
declare module "@mui/styles/defaultTheme" {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface DefaultTheme extends Theme {}
|
||||
}
|
||||
|
||||
const GlobalCss = withStyles({
|
||||
// @global is handled by jss-plugin-global.
|
||||
"@global": {
|
||||
body: {
|
||||
height: "100vh",
|
||||
width: "100vw",
|
||||
fontFamily: "Lato, sans-serif",
|
||||
},
|
||||
"#root": {
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
alignItems: "stretch",
|
||||
},
|
||||
".min-icon": {
|
||||
// height: 26,
|
||||
width: 26,
|
||||
},
|
||||
".MuiButton-endIcon": {
|
||||
"& .min-icon": {
|
||||
// height: 26,
|
||||
width: 16,
|
||||
},
|
||||
},
|
||||
// You should target [class*="MuiButton-root"] instead if you nest themes.
|
||||
".MuiButton-root:not(.noDefaultHeight)": {
|
||||
height: 38,
|
||||
},
|
||||
".MuiButton-contained": {
|
||||
fontSize: "14px",
|
||||
textTransform: "capitalize",
|
||||
padding: "15px 25px 15px 25px",
|
||||
borderRadius: 3,
|
||||
},
|
||||
".MuiButton-sizeSmall": {
|
||||
padding: "4px 10px",
|
||||
fontSize: "0.8125rem",
|
||||
},
|
||||
".MuiTableCell-head": {
|
||||
borderRadius: "3px 3px 0px 0px",
|
||||
fontSize: 13,
|
||||
},
|
||||
".MuiPaper-root": {
|
||||
borderRadius: 3,
|
||||
},
|
||||
".MuiDrawer-paperAnchorDockedLeft": {
|
||||
borderRight: 0,
|
||||
},
|
||||
".MuiDrawer-root": {
|
||||
"& .MuiPaper-root": {
|
||||
borderRadius: 0,
|
||||
},
|
||||
},
|
||||
hr: {
|
||||
borderTop: 0,
|
||||
borderLeft: 0,
|
||||
borderRight: 0,
|
||||
borderColor: "#999999",
|
||||
backgroundColor: "transparent" as const,
|
||||
},
|
||||
ul: {
|
||||
paddingLeft: 20,
|
||||
listStyle: "none" /* Remove default bullets */,
|
||||
"& li::before:not(.Mui*)": {
|
||||
content: '"■"',
|
||||
color: "#2781B0",
|
||||
fontSize: 20,
|
||||
display:
|
||||
"inline-block" /* Needed to add space between the bullet and the text */,
|
||||
width: "1em" /* Also needed for space (tweak if needed) */,
|
||||
marginLeft: "-1em" /* Also needed for space (tweak if needed) */,
|
||||
},
|
||||
"& ul": {
|
||||
listStyle: "none" /* Remove default bullets */,
|
||||
"& li::before:not(.Mui*)": {
|
||||
content: '"○"',
|
||||
color: "#2781B0",
|
||||
fontSize: 20,
|
||||
display:
|
||||
"inline-block" /* Needed to add space between the bullet and the text */,
|
||||
width: "1em" /* Also needed for space (tweak if needed) */,
|
||||
marginLeft: "-1em" /* Also needed for space (tweak if needed) */,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})(() => null);
|
||||
import StyleHandler from "./StyleHandler";
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById("root") as HTMLElement
|
||||
@@ -134,12 +29,9 @@ const root = ReactDOM.createRoot(
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<GlobalCss />
|
||||
<StyledEngineProvider injectFirst>
|
||||
<ThemeProvider theme={theme}>
|
||||
<MainRouter />
|
||||
</ThemeProvider>
|
||||
</StyledEngineProvider>
|
||||
<StyleHandler>
|
||||
<MainRouter />
|
||||
</StyleHandler>
|
||||
</Provider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
@@ -43,7 +43,7 @@ import {
|
||||
} from "../../ObjectBrowser/objectBrowserSlice";
|
||||
import SearchBox from "../../Common/SearchBox";
|
||||
import { selFeatures } from "../../consoleSlice";
|
||||
import { LoginMinIOLogo } from "../../../../icons";
|
||||
import AutoColorIcon from "../../Common/Components/AutoColorIcon";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
@@ -150,14 +150,7 @@ const BrowserHandler = () => {
|
||||
}}
|
||||
>
|
||||
<Grid>
|
||||
<LoginMinIOLogo
|
||||
style={{
|
||||
width: 105,
|
||||
marginRight: 30,
|
||||
marginTop: 10,
|
||||
fill: "#081C42",
|
||||
}}
|
||||
/>
|
||||
<AutoColorIcon marginRight={30} marginTop={10} />
|
||||
</Grid>
|
||||
<Grid item xs>
|
||||
{searchBar}
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
AddIcon,
|
||||
BucketsIcon,
|
||||
LifecycleConfigIcon,
|
||||
LoginMinIOLogo,
|
||||
SelectAllIcon,
|
||||
} from "../../../../icons";
|
||||
import {
|
||||
@@ -60,6 +59,7 @@ import { setErrorSnackMessage } from "../../../../systemSlice";
|
||||
import { useAppDispatch } from "../../../../store";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selFeatures } from "../../consoleSlice";
|
||||
import AutoColorIcon from "../../Common/Components/AutoColorIcon";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
@@ -223,14 +223,7 @@ const ListBuckets = ({ classes }: IListBucketsProps) => {
|
||||
<Grid item xs={12} className={classes.actionsTray} display="flex">
|
||||
{obOnly && (
|
||||
<Grid item xs>
|
||||
<LoginMinIOLogo
|
||||
style={{
|
||||
width: 105,
|
||||
marginRight: 15,
|
||||
marginTop: 10,
|
||||
fill: "#081C42",
|
||||
}}
|
||||
/>
|
||||
<AutoColorIcon marginRight={15} marginTop={10} />
|
||||
</Grid>
|
||||
)}
|
||||
<SearchBox
|
||||
|
||||
@@ -17,23 +17,21 @@
|
||||
import React from "react";
|
||||
import { Theme } from "@mui/material/styles";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
import withStyles from "@mui/styles/withStyles";
|
||||
import { Grid, IconButton } from "@mui/material";
|
||||
import { ClosePanelIcon } from "../../../../../../icons";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
|
||||
interface IDetailsListPanel {
|
||||
classes: any;
|
||||
open: boolean;
|
||||
className?: string;
|
||||
closePanel: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
detailsList: {
|
||||
borderColor: "#EAEDEE",
|
||||
backgroundColor: "#fff",
|
||||
borderWidth: 0,
|
||||
borderStyle: "solid",
|
||||
borderRadius: 3,
|
||||
@@ -68,19 +66,23 @@ const styles = (theme: Theme) =>
|
||||
width: 14,
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const DetailsListPanel = ({
|
||||
classes,
|
||||
open,
|
||||
closePanel,
|
||||
className = "",
|
||||
children,
|
||||
}: IDetailsListPanel) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Grid
|
||||
item
|
||||
className={`${classes.detailsList} ${open ? "open" : ""} ${className}`}
|
||||
className={`${classes.detailsList} ${
|
||||
open ? "open" : ""
|
||||
} ${className} detailsListPanel`}
|
||||
>
|
||||
<IconButton onClick={closePanel} className={classes.closePanel}>
|
||||
<ClosePanelIcon />
|
||||
@@ -90,4 +92,4 @@ const DetailsListPanel = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default withStyles(styles)(DetailsListPanel);
|
||||
export default DetailsListPanel;
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React from "react";
|
||||
import { LoginMinIOLogo } from "../../../../icons";
|
||||
import { useSelector } from "react-redux";
|
||||
import { AppState } from "../../../../store";
|
||||
|
||||
interface IAutoColorIcon {
|
||||
marginRight: number;
|
||||
marginTop: number;
|
||||
}
|
||||
|
||||
const AutoColorIcon = ({ marginRight, marginTop }: IAutoColorIcon) => {
|
||||
let tinycolor = require("tinycolor2");
|
||||
|
||||
const colorVariants = useSelector(
|
||||
(state: AppState) => state.system.overrideStyles
|
||||
);
|
||||
|
||||
const isDark =
|
||||
tinycolor(colorVariants?.backgroundColor || "#fff").getBrightness() <= 128;
|
||||
|
||||
return (
|
||||
<LoginMinIOLogo
|
||||
style={{
|
||||
width: 105,
|
||||
marginRight,
|
||||
marginTop,
|
||||
fill: isDark ? "#fff" : "#081C42",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutoColorIcon;
|
||||
@@ -172,7 +172,6 @@ const styles = () =>
|
||||
".rowLine": {
|
||||
borderBottom: `1px solid ${borderColor}`,
|
||||
height: 40,
|
||||
color: "#393939",
|
||||
fontSize: 14,
|
||||
transitionDuration: 0.3,
|
||||
"&:focus": {
|
||||
|
||||
@@ -30,6 +30,7 @@ const initialState: ConsoleState = {
|
||||
distributedMode: false,
|
||||
permissions: {},
|
||||
allowResources: null,
|
||||
customStyles: null,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -32,4 +32,5 @@ export interface ISessionResponse {
|
||||
distributedMode: boolean;
|
||||
permissions: ISessionPermissions;
|
||||
allowResources: IAllowResources[] | null;
|
||||
customStyles?: string | null;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { snackBarMessage, SRInfoStateType } from "./types";
|
||||
import { ErrorResponseHandler } from "./common/types";
|
||||
import { ErrorResponseHandler, IEmbeddedCustomStyles } from "./common/types";
|
||||
import { AppState } from "./store";
|
||||
import { SubnetInfo } from "./screens/Console/License/types";
|
||||
|
||||
@@ -42,6 +42,7 @@ export interface SystemState {
|
||||
distributedSetup: boolean;
|
||||
siteReplicationInfo: SRInfoStateType;
|
||||
licenseInfo: null | SubnetInfo;
|
||||
overrideStyles: null | IEmbeddedCustomStyles;
|
||||
}
|
||||
|
||||
const initialState: SystemState = {
|
||||
@@ -70,6 +71,7 @@ const initialState: SystemState = {
|
||||
serverDiagnosticStatus: "",
|
||||
distributedSetup: false,
|
||||
licenseInfo: null,
|
||||
overrideStyles: null,
|
||||
};
|
||||
|
||||
export const systemSlice = createSlice({
|
||||
@@ -151,6 +153,12 @@ export const systemSlice = createSlice({
|
||||
setLicenseInfo: (state, action: PayloadAction<SubnetInfo | null>) => {
|
||||
state.licenseInfo = action.payload;
|
||||
},
|
||||
setOverrideStyles: (
|
||||
state,
|
||||
action: PayloadAction<IEmbeddedCustomStyles>
|
||||
) => {
|
||||
state.overrideStyles = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -172,6 +180,7 @@ export const {
|
||||
globalSetDistributedSetup,
|
||||
setSiteReplicationInfo,
|
||||
setLicenseInfo,
|
||||
setOverrideStyles,
|
||||
} = systemSlice.actions;
|
||||
|
||||
export const selDistSet = (state: AppState) => state.system.distributedSetup;
|
||||
|
||||
152
portal-ui/src/utils/stylesUtils.ts
Normal file
152
portal-ui/src/utils/stylesUtils.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { IEmbeddedCustomStyles } from "../common/types";
|
||||
import { createTheme } from "@mui/material";
|
||||
|
||||
export const getOverrideColorVariants: (
|
||||
customStyles: string
|
||||
) => false | IEmbeddedCustomStyles = (customStyles) => {
|
||||
try {
|
||||
return JSON.parse(atob(customStyles)) as IEmbeddedCustomStyles;
|
||||
} catch (e) {
|
||||
console.error("Error processing override styles, skipping.", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const generateOverrideTheme = (overrideVars: IEmbeddedCustomStyles) => {
|
||||
const theme = createTheme({
|
||||
palette: {
|
||||
primary: {
|
||||
light: overrideVars.buttonStyles.hoverColor || "#073052",
|
||||
main: overrideVars.buttonStyles.backgroundColor || "#081C42",
|
||||
dark: overrideVars.buttonStyles.activeColor || "#05122B",
|
||||
contrastText: overrideVars.buttonStyles.textColor || "#fff",
|
||||
},
|
||||
secondary: {
|
||||
light: "#ff7961",
|
||||
main: "#f44336",
|
||||
dark: "#ba000d",
|
||||
contrastText: "#000",
|
||||
},
|
||||
background: {
|
||||
default: overrideVars.backgroundColor,
|
||||
},
|
||||
success: {
|
||||
main: "#4ccb92",
|
||||
},
|
||||
warning: {
|
||||
main: "#FFBD62",
|
||||
},
|
||||
error: {
|
||||
light: "#e03a48",
|
||||
main: "#C83B51",
|
||||
contrastText: "#fff",
|
||||
},
|
||||
},
|
||||
typography: {
|
||||
fontFamily: ["Lato", "sans-serif"].join(","),
|
||||
h1: {
|
||||
fontWeight: "bold",
|
||||
color: overrideVars.fontColor,
|
||||
},
|
||||
h2: {
|
||||
fontWeight: "bold",
|
||||
color: overrideVars.fontColor,
|
||||
},
|
||||
h3: {
|
||||
fontWeight: "bold",
|
||||
color: overrideVars.fontColor,
|
||||
},
|
||||
h4: {
|
||||
fontWeight: "bold",
|
||||
color: overrideVars.fontColor,
|
||||
},
|
||||
h5: {
|
||||
fontWeight: "bold",
|
||||
color: overrideVars.fontColor,
|
||||
},
|
||||
h6: {
|
||||
fontWeight: "bold",
|
||||
color: overrideVars.fontColor,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
textTransform: "none",
|
||||
borderRadius: 3,
|
||||
height: 40,
|
||||
padding: "0 20px",
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
boxShadow: "none",
|
||||
"& .min-icon": {
|
||||
maxHeight: 18,
|
||||
},
|
||||
"&.MuiButton-contained.Mui-disabled": {
|
||||
backgroundColor: "#EAEDEE",
|
||||
fontWeight: 600,
|
||||
color: "#767676",
|
||||
},
|
||||
"& .MuiButton-iconSizeMedium > *:first-of-type": {
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiPaper: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: overrideVars.backgroundColor,
|
||||
color: overrideVars.fontColor,
|
||||
},
|
||||
elevation1: {
|
||||
boxShadow: "none",
|
||||
border: "#EAEDEE 1px solid",
|
||||
borderRadius: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiListItem: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
"&.MuiListItem-root.Mui-selected": {
|
||||
background: "inherit",
|
||||
"& .MuiTypography-root": {
|
||||
fontWeight: "bold",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTab: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
textTransform: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
colors: {
|
||||
link: "#2781B0",
|
||||
},
|
||||
});
|
||||
|
||||
return theme;
|
||||
};
|
||||
@@ -11038,6 +11038,11 @@ tiny-warning@^1.0.2, tiny-warning@^1.0.3:
|
||||
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
|
||||
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
|
||||
|
||||
tinycolor2@^1.4.2:
|
||||
version "1.4.2"
|
||||
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
|
||||
integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==
|
||||
|
||||
tmp-promise@^1.0.5:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-1.1.0.tgz#bb924d239029157b9bc1d506a6aa341f8b13e64c"
|
||||
|
||||
@@ -95,6 +95,7 @@ func configureAPI(api *operations.ConsoleAPI) http.Handler {
|
||||
AccountAccessKey: claims.AccountAccessKey,
|
||||
Hm: claims.HideMenu,
|
||||
Ob: claims.ObjectBrowser,
|
||||
CustomStyleOb: claims.CustomStyleOB,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -355,6 +356,8 @@ func handleSPA(w http.ResponseWriter, r *http.Request) {
|
||||
sts := r.URL.Query().Get("sts")
|
||||
stsAccessKey := r.URL.Query().Get("sts_a")
|
||||
stsSecretKey := r.URL.Query().Get("sts_s")
|
||||
overridenStyles := r.URL.Query().Get("ov_st")
|
||||
|
||||
// if these three parameters are present we are being asked to issue a session with these values
|
||||
if sts != "" && stsAccessKey != "" && stsSecretKey != "" {
|
||||
creds := credentials.NewStaticV4(stsAccessKey, stsSecretKey, sts)
|
||||
@@ -366,6 +369,14 @@ func handleSPA(w http.ResponseWriter, r *http.Request) {
|
||||
sf.HideMenu = true
|
||||
sf.ObjectBrowser = true
|
||||
|
||||
err := ValidateEncodedStyles(overridenStyles)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
} else {
|
||||
sf.CustomStyleOB = overridenStyles
|
||||
}
|
||||
|
||||
sessionID, err := login(consoleCreds, sf)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
|
||||
@@ -51,7 +51,6 @@ const (
|
||||
PrometheusExtraLabels = "CONSOLE_PROMETHEUS_EXTRA_LABELS"
|
||||
ConsoleLogQueryURL = "CONSOLE_LOG_QUERY_URL"
|
||||
ConsoleLogQueryAuthToken = "CONSOLE_LOG_QUERY_AUTH_TOKEN"
|
||||
ConsoleObjectBrowserOnly = "CONSOLE_OBJECT_BROWSER_ONLY"
|
||||
LogSearchQueryAuthToken = "LOGSEARCH_QUERY_AUTH_TOKEN"
|
||||
SlashSeparator = "/"
|
||||
)
|
||||
|
||||
@@ -6006,6 +6006,9 @@ func init() {
|
||||
"accountAccessKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"customStyleOb": {
|
||||
"type": "string"
|
||||
},
|
||||
"hm": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -6318,6 +6321,9 @@ func init() {
|
||||
"$ref": "#/definitions/permissionResource"
|
||||
}
|
||||
},
|
||||
"customStyles": {
|
||||
"type": "string"
|
||||
},
|
||||
"distributedMode": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -13266,6 +13272,9 @@ func init() {
|
||||
"accountAccessKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"customStyleOb": {
|
||||
"type": "string"
|
||||
},
|
||||
"hm": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -13578,6 +13587,9 @@ func init() {
|
||||
"$ref": "#/definitions/permissionResource"
|
||||
}
|
||||
},
|
||||
"customStyles": {
|
||||
"type": "string"
|
||||
},
|
||||
"distributedMode": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -103,6 +103,7 @@ func getSessionResponse(ctx context.Context, session *models.Principal) (*models
|
||||
}
|
||||
currTime := time.Now().UTC()
|
||||
|
||||
customStyles := session.CustomStyleOb
|
||||
// This actions will be global, meaning has to be attached to all resources
|
||||
conditionValues := map[string][]string{
|
||||
condition.AWSUsername.Name(): {session.AccountAccessKey},
|
||||
@@ -244,6 +245,7 @@ func getSessionResponse(ctx context.Context, session *models.Principal) (*models
|
||||
DistributedMode: erasure,
|
||||
Permissions: resourcePermissions,
|
||||
AllowResources: allowResources,
|
||||
CustomStyles: customStyles,
|
||||
}
|
||||
return sessionResp, nil
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ package restapi
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -40,6 +43,21 @@ import (
|
||||
// more likely then others.
|
||||
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ012345"
|
||||
|
||||
type CustomButtonStyle struct {
|
||||
BackgroundColor *string `json:"backgroundColor"`
|
||||
TextColor *string `json:"textColor"`
|
||||
HoverColor *string `json:"hoverColor"`
|
||||
HoverText *string `json:"hoverText"`
|
||||
ActiveColor *string `json:"activeColor"`
|
||||
ActiveText *string `json:"activeText"`
|
||||
}
|
||||
|
||||
type CustomStyles struct {
|
||||
BackgroundColor *string `json:"backgroundColor"`
|
||||
FontColor *string `json:"fontColor"`
|
||||
ButtonStyles *CustomButtonStyle `json:"buttonStyles"`
|
||||
}
|
||||
|
||||
func RandomCharStringWithAlphabet(n int, alphabet string) string {
|
||||
random := make([]byte, n)
|
||||
if _, err := io.ReadFull(rand.Reader, random); err != nil {
|
||||
@@ -130,6 +148,28 @@ func ExpireSessionCookie() http.Cookie {
|
||||
}
|
||||
}
|
||||
|
||||
func ValidateEncodedStyles(encodedStyles string) error {
|
||||
// encodedStyle JSON validation
|
||||
str, err := base64.StdEncoding.DecodeString(encodedStyles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var styleElements *CustomStyles
|
||||
|
||||
err = json.Unmarshal(str, &styleElements)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if styleElements.BackgroundColor == nil || styleElements.FontColor == nil || styleElements.ButtonStyles == nil {
|
||||
return errors.New("specified style is not in the correct format")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SanitizeEncodedPrefix replaces spaces for + since those are lost when you do GET parameters
|
||||
func SanitizeEncodedPrefix(rawPrefix string) string {
|
||||
return strings.ReplaceAll(rawPrefix, " ", "+")
|
||||
|
||||
@@ -3729,6 +3729,8 @@ definitions:
|
||||
type: boolean
|
||||
ob:
|
||||
type: boolean
|
||||
customStyleOb:
|
||||
type: string
|
||||
startProfilingItem:
|
||||
type: object
|
||||
properties:
|
||||
@@ -3776,6 +3778,8 @@ definitions:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
customStyles:
|
||||
type: string
|
||||
allowResources:
|
||||
type: array
|
||||
items:
|
||||
|
||||
Reference in New Issue
Block a user