Added Rename modal for filenames longer than 200 characters in Windows (#2137)

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2022-06-22 12:43:57 -05:00
committed by GitHub
parent 618a00d775
commit 2ad42d660b
17 changed files with 858 additions and 453 deletions

73
models/user_s_as.go Normal file
View File

@@ -0,0 +1,73 @@
// Code generated by go-swagger; DO NOT EDIT.
// 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/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// UserSAs user s as
//
// swagger:model userSAs
type UserSAs struct {
// path
Path string `json:"path,omitempty"`
// recursive
Recursive bool `json:"recursive,omitempty"`
// version ID
VersionID string `json:"versionID,omitempty"`
}
// Validate validates this user s as
func (m *UserSAs) Validate(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this user s as based on context it is used
func (m *UserSAs) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *UserSAs) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *UserSAs) UnmarshalBinary(b []byte) error {
var res UserSAs
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -709,3 +709,13 @@ export const capacityColors = (usedSpace: number, maxSpace: number) => {
return "#07193E";
};
export const getClientOS = (): string => {
const getPlatform = get(window.navigator, "platform", "undefined");
if (!getPlatform) {
return "undefined";
}
return getPlatform;
};

View File

@@ -767,6 +767,7 @@ const ListObjects = () => {
encodeURLString(object.name),
object.version_id,
object.size,
null,
(progress) => {
dispatch(
updateProgress({

View File

@@ -35,6 +35,7 @@ import { ErrorResponseHandler } from "../../../../../../common/types";
import {
decodeURLString,
encodeURLString,
getClientOS,
niceBytes,
niceBytesInt,
niceDaysInt,
@@ -87,6 +88,7 @@ import {
setVersionsModeEnabled,
updateProgress,
} from "../../../../ObjectBrowser/objectBrowserSlice";
import RenameLongFileName from "../../../../ObjectBrowser/RenameLongFilename";
const styles = () =>
createStyles({
@@ -155,7 +157,6 @@ const ObjectDetailPanel = ({
bucketName,
versioning,
locking,
onClosePanel,
}: IObjectDetailPanelProps) => {
const dispatch = useAppDispatch();
@@ -183,6 +184,7 @@ const ObjectDetailPanel = ({
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [previewOpen, setPreviewOpen] = useState<boolean>(false);
const [totalVersionsSize, setTotalVersionsSize] = useState<number>(0);
const [longFileOpen, setLongFileOpen] = useState<boolean>(false);
const internalPathsDecoded = decodeURLString(internalPaths) || "";
const allPathData = internalPathsDecoded.split("/");
@@ -282,16 +284,29 @@ const ObjectDetailPanel = ({
setShareFileModalOpen(false);
};
const closeFileOpen = () => {
setLongFileOpen(false);
};
const downloadObject = (object: IFileInfo) => {
const identityDownload = encodeURLString(
`${bucketName}-${object.name}-${new Date().getTime()}-${Math.random()}`
);
if (
object.name.length > 200 &&
getClientOS().toLowerCase().includes("win")
) {
setLongFileOpen(true);
return;
}
const downloadCall = download(
bucketName,
internalPaths,
object.version_id,
parseInt(object.size || "0"),
null,
(progress) => {
dispatch(
updateProgress({
@@ -577,6 +592,16 @@ const ObjectDetailPanel = ({
closeInspectModalAndRefresh={closeInspectModal}
/>
)}
{longFileOpen && actualInfo && (
<RenameLongFileName
open={longFileOpen}
closeModal={closeFileOpen}
currentItem={currentItem}
bucketName={bucketName}
internalPaths={internalPaths}
actualInfo={actualInfo}
/>
)}
{loadingObjectInfo ? (
<Fragment>{loaderForContainer}</Fragment>

View File

@@ -251,6 +251,7 @@ const VersionsNavigator = ({
internalPaths,
object.version_id,
parseInt(object.size || "0"),
null,
(progress) => {
dispatch(
updateProgress({

View File

@@ -16,12 +16,14 @@
import { BucketObjectItem } from "./ListObjects/types";
import { IAllowResources } from "../../../types";
import { encodeURLString } from "../../../../../common/utils";
export const download = (
bucketName: string,
objectPath: string,
versionID: any,
fileSize: number,
overrideFileName: string | null = null,
progressCallback: (progress: number) => void,
completeCallback: () => void,
errorCallback: () => void,
@@ -29,7 +31,11 @@ export const download = (
) => {
const anchor = document.createElement("a");
document.body.appendChild(anchor);
let path = `/api/v1/buckets/${bucketName}/objects/download?prefix=${objectPath}`;
let path = `/api/v1/buckets/${bucketName}/objects/download?prefix=${objectPath}${
overrideFileName !== null && overrideFileName.trim() !== ""
? `&override_file_name=${encodeURLString(overrideFileName || "")}`
: ""
}`;
if (versionID) {
path = path.concat(`&version_id=${versionID}`);
}

View File

@@ -0,0 +1,185 @@
// 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, { useState } from "react";
import Grid from "@mui/material/Grid";
import createStyles from "@mui/styles/createStyles";
import { Button } from "@mui/material";
import makeStyles from "@mui/styles/makeStyles";
import { Theme } from "@mui/material/styles";
import { EditIcon } from "../../../icons";
import {
containerForHeader,
formFieldStyles,
modalStyleUtils,
spacingUtils,
} from "../Common/FormComponents/common/styleLibrary";
import { IFileInfo } from "../Buckets/ListBuckets/Objects/ObjectDetails/types";
import { encodeURLString } from "../../../common/utils";
import { download } from "../Buckets/ListBuckets/Objects/utils";
import {
cancelObjectInList,
completeObject,
failObject,
setNewObject,
updateProgress,
} from "./objectBrowserSlice";
import { makeid, storeCallForObjectWithID } from "./transferManager";
import { useAppDispatch } from "../../../store";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
interface IRenameLongFilename {
open: boolean;
bucketName: string;
internalPaths: string;
currentItem: string;
actualInfo: IFileInfo;
closeModal: () => void;
}
const useStyles = makeStyles((theme: Theme) =>
createStyles({
...modalStyleUtils,
...formFieldStyles,
...spacingUtils,
...containerForHeader(theme.spacing(4)),
})
);
const RenameLongFileName = ({
open,
closeModal,
currentItem,
internalPaths,
actualInfo,
bucketName,
}: IRenameLongFilename) => {
const classes = useStyles();
const dispatch = useAppDispatch();
const [newFileName, setNewFileName] = useState(currentItem);
const doDownload = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const identityDownload = encodeURLString(
`${bucketName}-${
actualInfo.name
}-${new Date().getTime()}-${Math.random()}`
);
const downloadCall = download(
bucketName,
internalPaths,
actualInfo.version_id,
parseInt(actualInfo.size || "0"),
newFileName,
(progress) => {
dispatch(
updateProgress({
instanceID: identityDownload,
progress: progress,
})
);
},
() => {
dispatch(completeObject(identityDownload));
},
() => {
dispatch(failObject(identityDownload));
},
() => {
dispatch(cancelObjectInList(identityDownload));
}
);
const ID = makeid(8);
storeCallForObjectWithID(ID, downloadCall);
dispatch(
setNewObject({
ID,
bucketName,
done: false,
instanceID: identityDownload,
percentage: 0,
prefix: newFileName,
type: "download",
waitingForFile: true,
failed: false,
cancelled: false,
})
);
downloadCall.send();
closeModal();
};
return (
<ModalWrapper
title={`Rename Download`}
modalOpen={open}
onClose={closeModal}
titleIcon={<EditIcon />}
>
<div>
The file you are trying to download has a long name.
<br />
This can cause issues on Windows Systems by trimming the file name after
download.
<br />
<br /> Would you like to rename the file for this download?
</div>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
doDownload(e);
}}
>
<Grid container>
<Grid item xs={12} className={classes.modalFormScrollable}>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
id="download-filename"
name="download-filename"
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setNewFileName(event.target.value);
}}
label="Filename for download"
type={"text"}
value={newFileName}
/>
</Grid>
</Grid>
{newFileName.length > 200 && (
<Grid item xs={12}>
We suggest filename to be less than 200 characters long. <br />
Are you sure you want to continue?
</Grid>
)}
<Grid item xs={12} className={classes.modalButtonBar}>
<Button type="submit" variant="contained" color="primary">
Download File
</Button>
</Grid>
</Grid>
</form>
</ModalWrapper>
);
};
export default RenameLongFileName;

View File

@@ -38,23 +38,27 @@ import Grid from "@mui/material/Grid";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import { Button, DialogContentText } from "@mui/material";
import ConfirmDialog from "../../Common/ModalWrapper/ConfirmDialog";
import { setErrorSnackMessage, setSnackBarMessage } from "../../../../systemSlice";
import {
setErrorSnackMessage,
setSnackBarMessage,
} from "../../../../systemSlice";
import { IKeyValue } from "../ListTenants/types";
import KeyPairEdit from "./KeyPairEdit";
import InputUnitMenu from "../../Common/FormComponents/InputUnitMenu/InputUnitMenu";
import { ITenantMonitoringStruct } from "../ListTenants/types";
import {setPrometheusEnabled,
setImage,
setSidecarImage,
setInitImage,
setStorageClassName,
setDiskCapacityGB,
setServiceAccountName,
setCPURequest,
setMemRequest,
} from "../TenantDetails/tenantMonitoringSlice"
import {
setPrometheusEnabled,
setImage,
setSidecarImage,
setInitImage,
setStorageClassName,
setDiskCapacityGB,
setServiceAccountName,
setCPURequest,
setMemRequest,
} from "../TenantDetails/tenantMonitoringSlice";
import { clearValidationError } from "../utils";
import { clearValidationError } from "../utils";
interface ITenantMonitoring {
classes: any;
@@ -87,105 +91,136 @@ const styles = (theme: Theme) =>
const TenantMonitoring = ({ classes }: ITenantMonitoring) => {
const dispatch = useAppDispatch();
const { tenantName, tenantNamespace } = useParams();
const prometheusEnabled = useSelector((state: AppState) => state.editTenantMonitoring.prometheusEnabled)
const image = useSelector((state: AppState) => state.editTenantMonitoring.image)
const sidecarImage = useSelector((state: AppState) => state.editTenantMonitoring.sidecarImage)
const initImage = useSelector((state: AppState) => state.editTenantMonitoring.initImage)
const diskCapacityGB = useSelector((state: AppState) => state.editTenantMonitoring.diskCapacityGB)
const cpuRequest = useSelector((state: AppState) => state.editTenantMonitoring.monitoringCPURequest)
const memRequest = useSelector((state: AppState) => state.editTenantMonitoring.monitoringMemRequest)
const serviceAccountName = useSelector((state: AppState) => state.editTenantMonitoring.serviceAccountName)
const storageClassName = useSelector((state: AppState) => state.editTenantMonitoring.storageClassName)
const prometheusEnabled = useSelector(
(state: AppState) => state.editTenantMonitoring.prometheusEnabled
);
const image = useSelector(
(state: AppState) => state.editTenantMonitoring.image
);
const sidecarImage = useSelector(
(state: AppState) => state.editTenantMonitoring.sidecarImage
);
const initImage = useSelector(
(state: AppState) => state.editTenantMonitoring.initImage
);
const diskCapacityGB = useSelector(
(state: AppState) => state.editTenantMonitoring.diskCapacityGB
);
const cpuRequest = useSelector(
(state: AppState) => state.editTenantMonitoring.monitoringCPURequest
);
const memRequest = useSelector(
(state: AppState) => state.editTenantMonitoring.monitoringMemRequest
);
const serviceAccountName = useSelector(
(state: AppState) => state.editTenantMonitoring.serviceAccountName
);
const storageClassName = useSelector(
(state: AppState) => state.editTenantMonitoring.storageClassName
);
const [validationErrors, setValidationErrors] = useState<any>({});
const [toggleConfirmOpen, setToggleConfirmOpen] = useState<boolean>(false);
const [labels, setLabels] = useState<IKeyValue[]>([{ key: "", value: "" }] );
const [annotations, setAnnotations] = useState<IKeyValue[]>( [{ key: "", value: "" }] );
const [nodeSelector, setNodeSelector] = useState<IKeyValue[]>([{ key: "", value: "" }] );
const [labels, setLabels] = useState<IKeyValue[]>([{ key: "", value: "" }]);
const [annotations, setAnnotations] = useState<IKeyValue[]>([
{ key: "", value: "" },
]);
const [nodeSelector, setNodeSelector] = useState<IKeyValue[]>([
{ key: "", value: "" },
]);
const [refreshMonitoringInfo, setRefreshMonitoringInfo] =
const [refreshMonitoringInfo, setRefreshMonitoringInfo] =
useState<boolean>(true);
const [labelsError, setLabelsError] = useState<any>({});
const [annotationsError, setAnnotationsError] = useState<any>({});
const [nodeSelectorError, setNodeSelectorError] = useState<any>({});
const [labelsError, setLabelsError] = useState<any>({});
const [annotationsError, setAnnotationsError] = useState<any>({});
const [nodeSelectorError, setNodeSelectorError] = useState<any>({});
const cleanValidation = (fieldName: string) => {
setValidationErrors(clearValidationError(validationErrors, fieldName));
};
const setMonitoringInfo = (res : ITenantMonitoringStruct) => {
dispatch(setImage(res.image));
dispatch(setSidecarImage(res.sidecarImage));
dispatch(setInitImage(res.initImage));
dispatch(setStorageClassName(res.storageClassName));
dispatch(setDiskCapacityGB(res.diskCapacityGB));
dispatch(setServiceAccountName(res.serviceAccountName));
dispatch(setCPURequest(res.monitoringCPURequest));
if (res.monitoringMemRequest) {
dispatch(setMemRequest(Math.floor(parseInt(res.monitoringMemRequest, 10) / 1000000000).toString()));
} else {
dispatch(setMemRequest("0"));
}
res.labels != null ? setLabels(res.labels) : setLabels([{key:"", value:""}]);
res.annotations != null ? setAnnotations(res.annotations) :setAnnotations([{key:"", value:""}]);
res.nodeSelector != null ? setNodeSelector(res.nodeSelector) :setNodeSelector([{key:"", value:""}]);
}
const cleanValidation = (fieldName: string) => {
setValidationErrors(clearValidationError(validationErrors, fieldName));
};
const trim = (x: IKeyValue[]): IKeyValue[] => {
let retval: IKeyValue[] = [];
for (let i = 0; i < x.length; i++) {
if (x[i].key !== "") {
retval.push(x[i]);
}
}
return retval;
};
const setMonitoringInfo = (res: ITenantMonitoringStruct) => {
dispatch(setImage(res.image));
dispatch(setSidecarImage(res.sidecarImage));
dispatch(setInitImage(res.initImage));
dispatch(setStorageClassName(res.storageClassName));
dispatch(setDiskCapacityGB(res.diskCapacityGB));
dispatch(setServiceAccountName(res.serviceAccountName));
dispatch(setCPURequest(res.monitoringCPURequest));
if (res.monitoringMemRequest) {
dispatch(
setMemRequest(
Math.floor(
parseInt(res.monitoringMemRequest, 10) / 1000000000
).toString()
)
);
} else {
dispatch(setMemRequest("0"));
}
res.labels != null
? setLabels(res.labels)
: setLabels([{ key: "", value: "" }]);
res.annotations != null
? setAnnotations(res.annotations)
: setAnnotations([{ key: "", value: "" }]);
res.nodeSelector != null
? setNodeSelector(res.nodeSelector)
: setNodeSelector([{ key: "", value: "" }]);
};
const checkValid = (): boolean => {
if (
Object.keys(validationErrors).length !== 0 ||
Object.keys(labelsError).length !== 0 ||
Object.keys(annotationsError).length !== 0 ||
Object.keys(nodeSelectorError).length !== 0
) {
let err: ErrorResponseHandler = {
errorMessage: "Invalid entry",
detailedError: "",
};
dispatch(setErrorSnackMessage(err));
return false;
} else {
return true;
const trim = (x: IKeyValue[]): IKeyValue[] => {
let retval: IKeyValue[] = [];
for (let i = 0; i < x.length; i++) {
if (x[i].key !== "") {
retval.push(x[i]);
}
};
useEffect(() => {
if (refreshMonitoringInfo) {
api
.invoke(
"GET",
`/api/v1/namespaces/${tenantNamespace || ""}/tenants/${
tenantName || ""
}/monitoring`
)
.then((res: ITenantMonitoringStruct) => {
dispatch(setPrometheusEnabled(res.prometheusEnabled));
setMonitoringInfo(res);
setRefreshMonitoringInfo(false);
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
setRefreshMonitoringInfo(false);
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [refreshMonitoringInfo]);
}
return retval;
};
const submitMonitoringInfo = () => {
if(checkValid()){
const checkValid = (): boolean => {
if (
Object.keys(validationErrors).length !== 0 ||
Object.keys(labelsError).length !== 0 ||
Object.keys(annotationsError).length !== 0 ||
Object.keys(nodeSelectorError).length !== 0
) {
let err: ErrorResponseHandler = {
errorMessage: "Invalid entry",
detailedError: "",
};
dispatch(setErrorSnackMessage(err));
return false;
} else {
return true;
}
};
useEffect(() => {
if (refreshMonitoringInfo) {
api
.invoke(
"GET",
`/api/v1/namespaces/${tenantNamespace || ""}/tenants/${
tenantName || ""
}/monitoring`
)
.then((res: ITenantMonitoringStruct) => {
dispatch(setPrometheusEnabled(res.prometheusEnabled));
setMonitoringInfo(res);
setRefreshMonitoringInfo(false);
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
setRefreshMonitoringInfo(false);
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [refreshMonitoringInfo]);
const submitMonitoringInfo = () => {
if (checkValid()) {
api
.invoke(
"PUT",
@@ -209,36 +244,35 @@ const TenantMonitoring = ({ classes }: ITenantMonitoring) => {
dispatch(setSnackBarMessage(`Prometheus configuration updated.`));
})
.catch((err: ErrorResponseHandler) => {
setErrorSnackMessage(err)
setErrorSnackMessage(err);
});
}
};
}
};
const togglePrometheus = () => {
const configInfo = {
prometheusEnabled: prometheusEnabled ,
toggle: true,
};
api
.invoke(
"PUT",
`/api/v1/namespaces/${tenantNamespace}/tenants/${tenantName}/monitoring`,
configInfo
)
.then(() => {
dispatch(setPrometheusEnabled(!prometheusEnabled));
setRefreshMonitoringInfo(true);
setToggleConfirmOpen(false);
setRefreshMonitoringInfo(true);
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
});
const togglePrometheus = () => {
const configInfo = {
prometheusEnabled: prometheusEnabled,
toggle: true,
};
api
.invoke(
"PUT",
`/api/v1/namespaces/${tenantNamespace}/tenants/${tenantName}/monitoring`,
configInfo
)
.then(() => {
dispatch(setPrometheusEnabled(!prometheusEnabled));
setRefreshMonitoringInfo(true);
setToggleConfirmOpen(false);
setRefreshMonitoringInfo(true);
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
});
};
return (
<Fragment>
{toggleConfirmOpen && (
<ConfirmDialog
isOpen={toggleConfirmOpen}
@@ -264,7 +298,7 @@ const TenantMonitoring = ({ classes }: ITenantMonitoring) => {
<Grid item xs>
<h1 className={classes.sectionTitle}>Prometheus Monitoring </h1>
</Grid>
<Grid item xs={7} justifyContent={"end"} textAlign={"right"}>
<Grid item xs={7} justifyContent={"end"} textAlign={"right"}>
<FormSwitchWrapper
label={""}
indicatorLabels={["Enabled", "Disabled"]}
@@ -281,176 +315,174 @@ const TenantMonitoring = ({ classes }: ITenantMonitoring) => {
<Grid xs={12}>
<hr className={classes.hrClass} />
</Grid>
</Grid>
{prometheusEnabled && (
<Fragment>
<Grid item xs={12} paddingBottom={2}>
<InputBoxWrapper
id={`image`}
label={"Image"}
placeholder={"quay.io/prometheus/prometheus:latest"}
name={`image`}
value={image}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
</Grid>
{prometheusEnabled && (
<Fragment>
<Grid item xs={12} paddingBottom={2}>
<InputBoxWrapper
id={`image`}
label={"Image"}
placeholder={"quay.io/prometheus/prometheus:latest"}
name={`image`}
value={image}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.validity.valid) {
dispatch(setImage(event.target.value));
}
cleanValidation(`image`)
}}
key={`image`}
pattern={"^[a-zA-Z0-9-./:]{1,253}$"}
error={validationErrors[`image`] || ""}
/>
</Grid>
<Grid item xs={12} paddingBottom={2}>
<InputBoxWrapper
id={`sidecarImage`}
label={"Sidecar Image"}
placeholder={"library/alpine:latest"}
name={`sidecarImage`}
value={sidecarImage}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.validity.valid) {
cleanValidation(`image`);
}}
key={`image`}
pattern={"^[a-zA-Z0-9-./:]{1,253}$"}
error={validationErrors[`image`] || ""}
/>
</Grid>
<Grid item xs={12} paddingBottom={2}>
<InputBoxWrapper
id={`sidecarImage`}
label={"Sidecar Image"}
placeholder={"library/alpine:latest"}
name={`sidecarImage`}
value={sidecarImage}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.validity.valid) {
dispatch(setSidecarImage(event.target.value));
}
cleanValidation(`sidecarImage`)
}}
key={`sidecarImage`}
pattern={"^[a-zA-Z0-9-./:]{1,253}$"}
error={validationErrors[`sidecarImage`] || ""}
/>
</Grid>
<Grid item xs={12} paddingBottom={2}>
<InputBoxWrapper
id={`initImage`}
label={"Init Image"}
placeholder={"library/busybox:1.33.1"}
name={`initImage`}
value={initImage}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.validity.valid) {
}
cleanValidation(`sidecarImage`);
}}
key={`sidecarImage`}
pattern={"^[a-zA-Z0-9-./:]{1,253}$"}
error={validationErrors[`sidecarImage`] || ""}
/>
</Grid>
<Grid item xs={12} paddingBottom={2}>
<InputBoxWrapper
id={`initImage`}
label={"Init Image"}
placeholder={"library/busybox:1.33.1"}
name={`initImage`}
value={initImage}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.validity.valid) {
dispatch(setInitImage(event.target.value));
}
cleanValidation(`initImage`)
}}
key={`initImage`}
pattern={"^[a-zA-Z0-9-./:]{1,253}$"}
error={validationErrors[`initImage`] || ""}
/>
</Grid>
<Grid item xs={12} paddingBottom={2}>
<InputBoxWrapper
id={`diskCapacityGB`}
label={"Disk Capacity"}
placeholder={"Disk Capacity"}
name={`diskCapacityGB`}
value={diskCapacityGB}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.validity.valid) {
cleanValidation(`initImage`);
}}
key={`initImage`}
pattern={"^[a-zA-Z0-9-./:]{1,253}$"}
error={validationErrors[`initImage`] || ""}
/>
</Grid>
<Grid item xs={12} paddingBottom={2}>
<InputBoxWrapper
id={`diskCapacityGB`}
label={"Disk Capacity"}
placeholder={"Disk Capacity"}
name={`diskCapacityGB`}
value={diskCapacityGB}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.validity.valid) {
dispatch(setDiskCapacityGB(event.target.value));
}
cleanValidation(`diskCapacityGB`)
}}
key={`diskCapacityGB`}
pattern={"[0-9]*"}
error={validationErrors[`diskCapacityGB`] || ""}
overlayObject={
<InputUnitMenu
id={"size-unit"}
onUnitChange={() => {}}
unitSelected={"Gi"}
unitsList={[{ label: "Gi", value: "Gi" }]}
disabled={true}
/>
}
/>
</Grid>
<Grid item xs={12} paddingBottom={2}>
<InputBoxWrapper
id={`cpuRequest`}
label={"CPU Request"}
placeholder={"CPU Request"}
name={`cpuRequest`}
value={cpuRequest}
pattern={"[0-9]*"}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.validity.valid) {
cleanValidation(`diskCapacityGB`);
}}
key={`diskCapacityGB`}
pattern={"[0-9]*"}
error={validationErrors[`diskCapacityGB`] || ""}
overlayObject={
<InputUnitMenu
id={"size-unit"}
onUnitChange={() => {}}
unitSelected={"Gi"}
unitsList={[{ label: "Gi", value: "Gi" }]}
disabled={true}
/>
}
/>
</Grid>
<Grid item xs={12} paddingBottom={2}>
<InputBoxWrapper
id={`cpuRequest`}
label={"CPU Request"}
placeholder={"CPU Request"}
name={`cpuRequest`}
value={cpuRequest}
pattern={"[0-9]*"}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.validity.valid) {
dispatch(setCPURequest(event.target.value));
}
cleanValidation(`cpuRequest`)
}}
key={`cpuRequest`}
error={validationErrors[`cpuRequest`] || ""}
/>
</Grid>
<Grid item xs={12} paddingBottom={2}>
<InputBoxWrapper
id={`memRequest`}
label={"Memory Request"}
placeholder={"Memory request"}
name={`memRequest`}
value={memRequest}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.validity.valid) {
dispatch(setMemRequest(event.target.value));
}
cleanValidation(`memRequest`)
}}
pattern={"[0-9]*"}
key={`memRequest`}
error={validationErrors[`memRequest`] || ""}
overlayObject={
<InputUnitMenu
id={"size-unit"}
onUnitChange={() => {}}
unitSelected={"Gi"}
unitsList={[{ label: "Gi", value: "Gi" }]}
disabled={true}
/>
}
/>
</Grid>
<Grid item xs={12} paddingBottom={2}>
<InputBoxWrapper
id={`serviceAccountName`}
label={"Service Account"}
placeholder={"Service Account Name"}
name={`serviceAccountName`}
value={serviceAccountName}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.validity.valid) {
cleanValidation(`cpuRequest`);
}}
key={`cpuRequest`}
error={validationErrors[`cpuRequest`] || ""}
/>
</Grid>
<Grid item xs={12} paddingBottom={2}>
<InputBoxWrapper
id={`memRequest`}
label={"Memory Request"}
placeholder={"Memory request"}
name={`memRequest`}
value={memRequest}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.validity.valid) {
dispatch(setMemRequest(event.target.value));
}
cleanValidation(`memRequest`);
}}
pattern={"[0-9]*"}
key={`memRequest`}
error={validationErrors[`memRequest`] || ""}
overlayObject={
<InputUnitMenu
id={"size-unit"}
onUnitChange={() => {}}
unitSelected={"Gi"}
unitsList={[{ label: "Gi", value: "Gi" }]}
disabled={true}
/>
}
/>
</Grid>
<Grid item xs={12} paddingBottom={2}>
<InputBoxWrapper
id={`serviceAccountName`}
label={"Service Account"}
placeholder={"Service Account Name"}
name={`serviceAccountName`}
value={serviceAccountName}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.validity.valid) {
dispatch(setServiceAccountName(event.target.value));
}
cleanValidation(`serviceAccountName`)
}
}
key={`serviceAccountName`}
pattern={"^[a-zA-Z0-9-.]{1,253}$"}
error={validationErrors[`serviceAccountName`] || ""}
/>
</Grid>
<Grid item xs={12} paddingBottom={2}>
<InputBoxWrapper
id={`storageClassName`}
label={"Storage Class"}
placeholder={"Storage Class Name"}
name={`storageClassName`}
value={storageClassName}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.validity.valid) {
}
cleanValidation(`serviceAccountName`);
}}
key={`serviceAccountName`}
pattern={"^[a-zA-Z0-9-.]{1,253}$"}
error={validationErrors[`serviceAccountName`] || ""}
/>
</Grid>
<Grid item xs={12} paddingBottom={2}>
<InputBoxWrapper
id={`storageClassName`}
label={"Storage Class"}
placeholder={"Storage Class Name"}
name={`storageClassName`}
value={storageClassName}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.validity.valid) {
dispatch(setStorageClassName(event.target.value));
}
cleanValidation(`storageClassName`)
}}
key={`storageClassName`}
pattern={"^[a-zA-Z0-9-.]{1,253}$"}
error={validationErrors[`storageClassName`] || ""}
/>
</Grid>
{labels !== null &&
<Grid item xs={12} className={classes.formFieldRow}>
}
cleanValidation(`storageClassName`);
}}
key={`storageClassName`}
pattern={"^[a-zA-Z0-9-.]{1,253}$"}
error={validationErrors[`storageClassName`] || ""}
/>
</Grid>
{labels !== null && (
<Grid item xs={12} className={classes.formFieldRow}>
<span className={classes.inputLabel}>Labels</span>
<KeyPairEdit
newValues={labels}
@@ -459,10 +491,11 @@ const TenantMonitoring = ({ classes }: ITenantMonitoring) => {
error={labelsError}
setError={setLabelsError}
/>
</Grid>}
{annotations !== null &&
<Grid item xs={12} className={classes.formFieldRow}>
</Grid>
)}
{annotations !== null && (
<Grid item xs={12} className={classes.formFieldRow}>
<span className={classes.inputLabel}>Annotations</span>
<KeyPairEdit
newValues={annotations}
@@ -471,9 +504,9 @@ const TenantMonitoring = ({ classes }: ITenantMonitoring) => {
error={annotationsError}
setError={setAnnotationsError}
/>
</Grid>
}
{nodeSelector !== null &&
</Grid>
)}
{nodeSelector !== null && (
<Grid item xs={12} className={classes.formFieldRow}>
<span className={classes.inputLabel}>Node Selector</span>
<KeyPairEdit
@@ -484,22 +517,20 @@ const TenantMonitoring = ({ classes }: ITenantMonitoring) => {
setError={setNodeSelectorError}
/>
</Grid>
}
<Grid item xs={12} textAlign={"right"}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={!checkValid()}
onClick={() =>
submitMonitoringInfo()
}
>
Save
</Button>
</Grid>
</Fragment>
)}
)}
<Grid item xs={12} textAlign={"right"}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={!checkValid()}
onClick={() => submitMonitoringInfo()}
>
Save
</Button>
</Grid>
</Fragment>
)}
</Fragment>
);
};

View File

@@ -82,73 +82,72 @@ const KeyPairEdit = ({
let keyValueInputs = newValues.map((_, index) => {
return (
<Fragment key={`keyvalue-${index.toString()}`} >
<Grid paddingBottom={1}>
<div className={classes.shortened} >
<InputBoxWrapper
id={`key-${index.toString()}`}
label={""}
placeholder={"Key"}
name={`key-${index.toString()}`}
value={newValues[index].key}
onChange={(e) => {
let tempLabels = [...newValues];
tempLabels[index].key = e.target.value;
setNewValues(tempLabels);
cleanValidation(`key-${index.toString()}`);
}}
index={index}
key={`csv-key-${index.toString()}`}
error={error[`key-${index.toString()}`] || ""}
/>
<InputBoxWrapper
id={`val-${index.toString()}`}
label={""}
placeholder={"Value"}
name={`val-${index.toString()}`}
value={newValues[index].value}
onChange={(e) => {
let tempLabels = [...newValues];
tempLabels[index].value = e.target.value;
setNewValues(tempLabels);
cleanValidation(`val-${index.toString()}`);
}}
index={index}
key={`csv-val-${index.toString()}`}
error={error[`val-${index.toString()}`] || ""}
/>
<Tooltip title={`Add ${paramName}`} aria-label="addlabel">
<IconButton
size={"small"}
onClick={() => {
<Fragment key={`keyvalue-${index.toString()}`}>
<Grid paddingBottom={1}>
<div className={classes.shortened}>
<InputBoxWrapper
id={`key-${index.toString()}`}
label={""}
placeholder={"Key"}
name={`key-${index.toString()}`}
value={newValues[index].key}
onChange={(e) => {
let tempLabels = [...newValues];
tempLabels.push({ key: "", value: "" });
tempLabels[index].key = e.target.value;
setNewValues(tempLabels);
cleanValidation(`key-${index.toString()}`);
}}
>
<AddIcon />
</IconButton>
</Tooltip>
<Tooltip title="Remove" aria-label="removeLabel">
<IconButton
size={"small"}
style={{ marginLeft: 16 }}
onClick={() => {
if (newValues.length === 1) {
setNewValues([{ key: "", value: "" }]);
}
if (newValues.length > 1) {
index={index}
key={`csv-key-${index.toString()}`}
error={error[`key-${index.toString()}`] || ""}
/>
<InputBoxWrapper
id={`val-${index.toString()}`}
label={""}
placeholder={"Value"}
name={`val-${index.toString()}`}
value={newValues[index].value}
onChange={(e) => {
let tempLabels = [...newValues];
tempLabels[index].value = e.target.value;
setNewValues(tempLabels);
cleanValidation(`val-${index.toString()}`);
}}
index={index}
key={`csv-val-${index.toString()}`}
error={error[`val-${index.toString()}`] || ""}
/>
<Tooltip title={`Add ${paramName}`} aria-label="addlabel">
<IconButton
size={"small"}
onClick={() => {
let tempLabels = [...newValues];
tempLabels.splice(index, 1);
tempLabels.push({ key: "", value: "" });
setNewValues(tempLabels);
}
}}
>
<DeleteIcon />
</IconButton>
</Tooltip>
</div>
}}
>
<AddIcon />
</IconButton>
</Tooltip>
<Tooltip title="Remove" aria-label="removeLabel">
<IconButton
size={"small"}
style={{ marginLeft: 16 }}
onClick={() => {
if (newValues.length === 1) {
setNewValues([{ key: "", value: "" }]);
}
if (newValues.length > 1) {
let tempLabels = [...newValues];
tempLabels.splice(index, 1);
setNewValues(tempLabels);
}
}}
>
<DeleteIcon />
</IconButton>
</Tooltip>
</div>
</Grid>
</Fragment>
);

View File

@@ -391,7 +391,10 @@ const TenantDetails = ({ classes }: ITenantDetailsProps) => {
<Route path={"pvcs/:PVCName"} element={<TenantVolumes />} />
<Route path={"volumes"} element={<VolumesSummary />} />
<Route path={"license"} element={<TenantLicense />} />
<Route path={"monitoring"} element={<EditTenantMonitoringScreen />} />
<Route
path={"monitoring"}
element={<EditTenantMonitoringScreen />}
/>
<Route path={"logging"} element={<TenantLogging />} />
<Route path={"events"} element={<TenantEvents />} />
<Route path={"csr"} element={<TenantCSR />} />

View File

@@ -17,91 +17,91 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { IKeyValue } from "../ListTenants/types";
export interface IEditTenantMonitoring {
prometheusEnabled: boolean;
image: string;
sidecarImage: string;
initImage: string;
storageClassName: string;
labels: IKeyValue[];
annotations: IKeyValue[];
nodeSelector: IKeyValue[];
diskCapacityGB: string;
serviceAccountName: string;
monitoringCPURequest: string;
monitoringMemRequest: string;
prometheusEnabled: boolean;
image: string;
sidecarImage: string;
initImage: string;
storageClassName: string;
labels: IKeyValue[];
annotations: IKeyValue[];
nodeSelector: IKeyValue[];
diskCapacityGB: string;
serviceAccountName: string;
monitoringCPURequest: string;
monitoringMemRequest: string;
}
const initialState: IEditTenantMonitoring = {
prometheusEnabled: false,
image: "",
sidecarImage: "",
initImage: "",
storageClassName: "",
labels: [{key:" ",value:" "}],
annotations: [{key:" ",value:" "}],
nodeSelector: [{key:" ",value:" "}],
diskCapacityGB: "0",
serviceAccountName: "",
monitoringCPURequest: "",
monitoringMemRequest: "",
prometheusEnabled: false,
image: "",
sidecarImage: "",
initImage: "",
storageClassName: "",
labels: [{ key: " ", value: " " }],
annotations: [{ key: " ", value: " " }],
nodeSelector: [{ key: " ", value: " " }],
diskCapacityGB: "0",
serviceAccountName: "",
monitoringCPURequest: "",
monitoringMemRequest: "",
};
export const editTenantMonitoringSlice = createSlice({
name: "editTenantMonitoring",
initialState,
reducers: {
setPrometheusEnabled: (state, action: PayloadAction<boolean>) => {
state.prometheusEnabled = action.payload;
},
setImage: (state, action: PayloadAction<string>) => {
state.image = action.payload;
},
setSidecarImage:(state, action: PayloadAction<string>) => {
state.sidecarImage = action.payload;
},
setInitImage: (state, action: PayloadAction<string>) => {
state.initImage = action.payload;
},
setStorageClassName: (state, action: PayloadAction<string>) => {
state.storageClassName = action.payload;
},
setLabels: (state, action: PayloadAction<IKeyValue[]>) => {
state.labels = action.payload;
},
setAnnotations: (state, action: PayloadAction<IKeyValue[]>) => {
state.annotations = action.payload;
},
setNodeSelector: (state, action: PayloadAction<IKeyValue[]>) => {
state.nodeSelector = action.payload;
},
setDiskCapacityGB: (state, action: PayloadAction<string>) => {
state.diskCapacityGB = action.payload;
},
setServiceAccountName: (state, action: PayloadAction<string>) => {
state.serviceAccountName = action.payload;
},
setCPURequest: (state, action: PayloadAction<string>) => {
state.monitoringCPURequest = action.payload;
},
setMemRequest: (state, action: PayloadAction<string>) => {
state.monitoringMemRequest = action.payload;
},
name: "editTenantMonitoring",
initialState,
reducers: {
setPrometheusEnabled: (state, action: PayloadAction<boolean>) => {
state.prometheusEnabled = action.payload;
},
setImage: (state, action: PayloadAction<string>) => {
state.image = action.payload;
},
setSidecarImage: (state, action: PayloadAction<string>) => {
state.sidecarImage = action.payload;
},
setInitImage: (state, action: PayloadAction<string>) => {
state.initImage = action.payload;
},
setStorageClassName: (state, action: PayloadAction<string>) => {
state.storageClassName = action.payload;
},
setLabels: (state, action: PayloadAction<IKeyValue[]>) => {
state.labels = action.payload;
},
setAnnotations: (state, action: PayloadAction<IKeyValue[]>) => {
state.annotations = action.payload;
},
setNodeSelector: (state, action: PayloadAction<IKeyValue[]>) => {
state.nodeSelector = action.payload;
},
setDiskCapacityGB: (state, action: PayloadAction<string>) => {
state.diskCapacityGB = action.payload;
},
setServiceAccountName: (state, action: PayloadAction<string>) => {
state.serviceAccountName = action.payload;
},
setCPURequest: (state, action: PayloadAction<string>) => {
state.monitoringCPURequest = action.payload;
},
setMemRequest: (state, action: PayloadAction<string>) => {
state.monitoringMemRequest = action.payload;
},
},
});
export const {
setPrometheusEnabled,
setImage,
setSidecarImage,
setInitImage,
setStorageClassName,
setLabels,
setAnnotations,
setNodeSelector,
setDiskCapacityGB,
setServiceAccountName,
setCPURequest,
setMemRequest,
setPrometheusEnabled,
setImage,
setSidecarImage,
setInitImage,
setStorageClassName,
setLabels,
setAnnotations,
setNodeSelector,
setDiskCapacityGB,
setServiceAccountName,
setCPURequest,
setMemRequest,
} = editTenantMonitoringSlice.actions;
export default editTenantMonitoringSlice.reducer;
export default editTenantMonitoringSlice.reducer;

View File

@@ -31,7 +31,7 @@ import createTenantReducer from "./screens/Console/Tenants/AddTenant/createTenan
import createUserReducer from "./screens/Console/Users/AddUsersSlice";
import addPoolReducer from "./screens/Console/Tenants/TenantDetails/Pools/AddPool/addPoolSlice";
import editPoolReducer from "./screens/Console/Tenants/TenantDetails/Pools/EditPool/editPoolSlice";
import editTenantMonitoringReducer from "./screens/Console/Tenants/TenantDetails/tenantMonitoringSlice"
import editTenantMonitoringReducer from "./screens/Console/Tenants/TenantDetails/tenantMonitoringSlice";
const rootReducer = combineReducers({
system: systemReducer,

View File

@@ -1585,6 +1585,12 @@ func init() {
"default": false,
"name": "preview",
"in": "query"
},
{
"type": "string",
"default": "",
"name": "override_file_name",
"in": "query"
}
],
"responses": {
@@ -8672,6 +8678,12 @@ func init() {
"default": false,
"name": "preview",
"in": "query"
},
{
"type": "string",
"default": "",
"name": "override_file_name",
"in": "query"
}
],
"responses": {

View File

@@ -40,10 +40,14 @@ func NewDownloadObjectParams() DownloadObjectParams {
var (
// initialize parameters with default values
overrideFileNameDefault = string("")
previewDefault = bool(false)
)
return DownloadObjectParams{
OverrideFileName: &overrideFileNameDefault,
Preview: &previewDefault,
}
}
@@ -62,6 +66,11 @@ type DownloadObjectParams struct {
In: path
*/
BucketName string
/*
In: query
Default: ""
*/
OverrideFileName *string
/*
Required: true
In: query
@@ -94,6 +103,11 @@ func (o *DownloadObjectParams) BindRequest(r *http.Request, route *middleware.Ma
res = append(res, err)
}
qOverrideFileName, qhkOverrideFileName, _ := qs.GetOK("override_file_name")
if err := o.bindOverrideFileName(qOverrideFileName, qhkOverrideFileName, route.Formats); err != nil {
res = append(res, err)
}
qPrefix, qhkPrefix, _ := qs.GetOK("prefix")
if err := o.bindPrefix(qPrefix, qhkPrefix, route.Formats); err != nil {
res = append(res, err)
@@ -128,6 +142,25 @@ func (o *DownloadObjectParams) bindBucketName(rawData []string, hasKey bool, for
return nil
}
// bindOverrideFileName binds and validates parameter OverrideFileName from query.
func (o *DownloadObjectParams) bindOverrideFileName(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: false
// AllowEmptyValue: false
if raw == "" { // empty values pass all other validations
// Default values have been previously initialized by NewDownloadObjectParams()
return nil
}
o.OverrideFileName = &raw
return nil
}
// bindPrefix binds and validates parameter Prefix from query.
func (o *DownloadObjectParams) bindPrefix(rawData []string, hasKey bool, formats strfmt.Registry) error {
if !hasKey {

View File

@@ -35,9 +35,10 @@ import (
type DownloadObjectURL struct {
BucketName string
Prefix string
Preview *bool
VersionID *string
OverrideFileName *string
Prefix string
Preview *bool
VersionID *string
_basePath string
// avoid unkeyed usage
@@ -80,6 +81,14 @@ func (o *DownloadObjectURL) Build() (*url.URL, error) {
qs := make(url.Values)
var overrideFileNameQ string
if o.OverrideFileName != nil {
overrideFileNameQ = *o.OverrideFileName
}
if overrideFileNameQ != "" {
qs.Set("override_file_name", overrideFileNameQ)
}
prefixQ := o.Prefix
if prefixQ != "" {
qs.Set("prefix", prefixQ)

View File

@@ -392,16 +392,27 @@ func getDownloadObjectResponse(session *models.Principal, params objectApi.Downl
defer resp.Close()
isPreview := params.Preview != nil && *params.Preview
// override filename is set
decodeOverride, err := base64.StdEncoding.DecodeString(*params.OverrideFileName)
if err != nil {
return
}
overrideName := string(decodeOverride)
// indicate it's a download / inline content to the browser, and the size of the object
var filename string
prefixElements := strings.Split(prefix, "/")
if len(prefixElements) > 0 {
if len(prefixElements) > 0 && overrideName == "" {
if prefixElements[len(prefixElements)-1] == "" {
filename = prefixElements[len(prefixElements)-2]
} else {
filename = prefixElements[len(prefixElements)-1]
}
} else if overrideName != "" {
filename = overrideName
}
escapedName := url.PathEscape(filename)
// indicate object size & content type

View File

@@ -1,3 +1,4 @@
swagger: "2.0"
info:
title: MinIO Console Server
@@ -432,6 +433,11 @@ paths:
required: false
type: boolean
default: false
- name: override_file_name
in: query
required: false
type: string
default: ""
responses:
200:
description: A successful response.