From 2ad42d660b5ad990289b30f9ec1908434dc0c51d Mon Sep 17 00:00:00 2001 From: Alex <33497058+bexsoft@users.noreply.github.com> Date: Wed, 22 Jun 2022 12:43:57 -0500 Subject: [PATCH] Added Rename modal for filenames longer than 200 characters in Windows (#2137) Signed-off-by: Benjamin Perez --- models/user_s_as.go | 73 ++ portal-ui/src/common/utils.ts | 10 + .../Objects/ListObjects/ListObjects.tsx | 1 + .../Objects/ListObjects/ObjectDetailPanel.tsx | 27 +- .../ObjectDetails/VersionsNavigator.tsx | 1 + .../Buckets/ListBuckets/Objects/utils.ts | 8 +- .../ObjectBrowser/RenameLongFilename.tsx | 185 +++++ .../EditTenantMonitoringScreen.tsx | 645 +++++++++--------- .../Tenants/TenantDetails/KeyPairEdit.tsx | 123 ++-- .../Tenants/TenantDetails/TenantDetails.tsx | 5 +- .../TenantDetails/tenantMonitoringSlice.ts | 152 ++--- portal-ui/src/store.ts | 2 +- restapi/embedded_spec.go | 12 + .../object/download_object_parameters.go | 33 + .../object/download_object_urlbuilder.go | 15 +- restapi/user_objects.go | 13 +- swagger-console.yml | 6 + 17 files changed, 858 insertions(+), 453 deletions(-) create mode 100644 models/user_s_as.go create mode 100644 portal-ui/src/screens/Console/ObjectBrowser/RenameLongFilename.tsx diff --git a/models/user_s_as.go b/models/user_s_as.go new file mode 100644 index 000000000..56f4fc370 --- /dev/null +++ b/models/user_s_as.go @@ -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 . +// + +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 +} diff --git a/portal-ui/src/common/utils.ts b/portal-ui/src/common/utils.ts index b57d52912..39fa40113 100644 --- a/portal-ui/src/common/utils.ts +++ b/portal-ui/src/common/utils.ts @@ -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; +}; diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx index 5bd36dd78..1d7a065fc 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx @@ -767,6 +767,7 @@ const ListObjects = () => { encodeURLString(object.name), object.version_id, object.size, + null, (progress) => { dispatch( updateProgress({ diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ObjectDetailPanel.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ObjectDetailPanel.tsx index 9a2ca0d44..9f711d219 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ObjectDetailPanel.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ObjectDetailPanel.tsx @@ -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(false); const [previewOpen, setPreviewOpen] = useState(false); const [totalVersionsSize, setTotalVersionsSize] = useState(0); + const [longFileOpen, setLongFileOpen] = useState(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 && ( + + )} {loadingObjectInfo ? ( {loaderForContainer} diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/VersionsNavigator.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/VersionsNavigator.tsx index c4c8f2173..d6ef32f91 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/VersionsNavigator.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/VersionsNavigator.tsx @@ -251,6 +251,7 @@ const VersionsNavigator = ({ internalPaths, object.version_id, parseInt(object.size || "0"), + null, (progress) => { dispatch( updateProgress({ diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/utils.ts b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/utils.ts index 6f0c463be..1a6ecc845 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/utils.ts +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/utils.ts @@ -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}`); } diff --git a/portal-ui/src/screens/Console/ObjectBrowser/RenameLongFilename.tsx b/portal-ui/src/screens/Console/ObjectBrowser/RenameLongFilename.tsx new file mode 100644 index 000000000..c3ee7668b --- /dev/null +++ b/portal-ui/src/screens/Console/ObjectBrowser/RenameLongFilename.tsx @@ -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 . + +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) => { + 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 ( + } + > +
+ The file you are trying to download has a long name. +
+ This can cause issues on Windows Systems by trimming the file name after + download. +
+
Would you like to rename the file for this download? +
+
) => { + doDownload(e); + }} + > + + + + ) => { + setNewFileName(event.target.value); + }} + label="Filename for download" + type={"text"} + value={newFileName} + /> + + + {newFileName.length > 200 && ( + + We suggest filename to be less than 200 characters long.
+ Are you sure you want to continue? +
+ )} + + + + +
+
+
+ ); +}; + +export default RenameLongFileName; diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/EditTenantMonitoringScreen.tsx b/portal-ui/src/screens/Console/Tenants/TenantDetails/EditTenantMonitoringScreen.tsx index 15b7557ad..9d17d2012 100644 --- a/portal-ui/src/screens/Console/Tenants/TenantDetails/EditTenantMonitoringScreen.tsx +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/EditTenantMonitoringScreen.tsx @@ -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({}); const [toggleConfirmOpen, setToggleConfirmOpen] = useState(false); - - const [labels, setLabels] = useState([{ key: "", value: "" }] ); - const [annotations, setAnnotations] = useState( [{ key: "", value: "" }] ); - const [nodeSelector, setNodeSelector] = useState([{ key: "", value: "" }] ); + const [labels, setLabels] = useState([{ key: "", value: "" }]); + const [annotations, setAnnotations] = useState([ + { key: "", value: "" }, + ]); + const [nodeSelector, setNodeSelector] = useState([ + { key: "", value: "" }, + ]); - - const [refreshMonitoringInfo, setRefreshMonitoringInfo] = + const [refreshMonitoringInfo, setRefreshMonitoringInfo] = useState(true); - const [labelsError, setLabelsError] = useState({}); - const [annotationsError, setAnnotationsError] = useState({}); - const [nodeSelectorError, setNodeSelectorError] = useState({}); + const [labelsError, setLabelsError] = useState({}); + const [annotationsError, setAnnotationsError] = useState({}); + const [nodeSelectorError, setNodeSelectorError] = useState({}); - 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 ( - {toggleConfirmOpen && ( {

Prometheus Monitoring

- + {
-
- - {prometheusEnabled && ( - - - - ) => { + + + {prometheusEnabled && ( + + + ) => { if (event.target.validity.valid) { dispatch(setImage(event.target.value)); } - cleanValidation(`image`) - }} - key={`image`} - pattern={"^[a-zA-Z0-9-./:]{1,253}$"} - error={validationErrors[`image`] || ""} - /> - - - ) => { - if (event.target.validity.valid) { + cleanValidation(`image`); + }} + key={`image`} + pattern={"^[a-zA-Z0-9-./:]{1,253}$"} + error={validationErrors[`image`] || ""} + /> + + + ) => { + if (event.target.validity.valid) { dispatch(setSidecarImage(event.target.value)); - } - cleanValidation(`sidecarImage`) - }} - key={`sidecarImage`} - pattern={"^[a-zA-Z0-9-./:]{1,253}$"} - error={validationErrors[`sidecarImage`] || ""} - /> - - - ) => { - if (event.target.validity.valid) { + } + cleanValidation(`sidecarImage`); + }} + key={`sidecarImage`} + pattern={"^[a-zA-Z0-9-./:]{1,253}$"} + error={validationErrors[`sidecarImage`] || ""} + /> + + + ) => { + if (event.target.validity.valid) { dispatch(setInitImage(event.target.value)); } - cleanValidation(`initImage`) - }} - key={`initImage`} - pattern={"^[a-zA-Z0-9-./:]{1,253}$"} - error={validationErrors[`initImage`] || ""} - /> - - - ) => { - if (event.target.validity.valid) { + cleanValidation(`initImage`); + }} + key={`initImage`} + pattern={"^[a-zA-Z0-9-./:]{1,253}$"} + error={validationErrors[`initImage`] || ""} + /> + + + ) => { + if (event.target.validity.valid) { dispatch(setDiskCapacityGB(event.target.value)); - } - cleanValidation(`diskCapacityGB`) - }} - key={`diskCapacityGB`} - pattern={"[0-9]*"} - error={validationErrors[`diskCapacityGB`] || ""} - overlayObject={ - {}} - unitSelected={"Gi"} - unitsList={[{ label: "Gi", value: "Gi" }]} - disabled={true} - /> } - /> - - - ) => { - if (event.target.validity.valid) { + cleanValidation(`diskCapacityGB`); + }} + key={`diskCapacityGB`} + pattern={"[0-9]*"} + error={validationErrors[`diskCapacityGB`] || ""} + overlayObject={ + {}} + unitSelected={"Gi"} + unitsList={[{ label: "Gi", value: "Gi" }]} + disabled={true} + /> + } + /> + + + ) => { + if (event.target.validity.valid) { dispatch(setCPURequest(event.target.value)); - } - cleanValidation(`cpuRequest`) - }} - key={`cpuRequest`} - error={validationErrors[`cpuRequest`] || ""} - /> - - - ) => { - if (event.target.validity.valid) { - dispatch(setMemRequest(event.target.value)); - } - cleanValidation(`memRequest`) - }} - pattern={"[0-9]*"} - key={`memRequest`} - error={validationErrors[`memRequest`] || ""} - overlayObject={ - {}} - unitSelected={"Gi"} - unitsList={[{ label: "Gi", value: "Gi" }]} - disabled={true} - /> } - /> - - - ) => { - if (event.target.validity.valid) { + cleanValidation(`cpuRequest`); + }} + key={`cpuRequest`} + error={validationErrors[`cpuRequest`] || ""} + /> + + + ) => { + if (event.target.validity.valid) { + dispatch(setMemRequest(event.target.value)); + } + cleanValidation(`memRequest`); + }} + pattern={"[0-9]*"} + key={`memRequest`} + error={validationErrors[`memRequest`] || ""} + overlayObject={ + {}} + unitSelected={"Gi"} + unitsList={[{ label: "Gi", value: "Gi" }]} + disabled={true} + /> + } + /> + + + ) => { + if (event.target.validity.valid) { dispatch(setServiceAccountName(event.target.value)); - } - cleanValidation(`serviceAccountName`) - } - } - key={`serviceAccountName`} - pattern={"^[a-zA-Z0-9-.]{1,253}$"} - error={validationErrors[`serviceAccountName`] || ""} - /> - - - ) => { - if (event.target.validity.valid) { + } + cleanValidation(`serviceAccountName`); + }} + key={`serviceAccountName`} + pattern={"^[a-zA-Z0-9-.]{1,253}$"} + error={validationErrors[`serviceAccountName`] || ""} + /> + + + ) => { + if (event.target.validity.valid) { dispatch(setStorageClassName(event.target.value)); - } - cleanValidation(`storageClassName`) - }} - key={`storageClassName`} - pattern={"^[a-zA-Z0-9-.]{1,253}$"} - error={validationErrors[`storageClassName`] || ""} - /> - - {labels !== null && - + } + cleanValidation(`storageClassName`); + }} + key={`storageClassName`} + pattern={"^[a-zA-Z0-9-.]{1,253}$"} + error={validationErrors[`storageClassName`] || ""} + /> + + {labels !== null && ( + Labels { error={labelsError} setError={setLabelsError} /> - } - - {annotations !== null && - + + )} + + {annotations !== null && ( + Annotations { error={annotationsError} setError={setAnnotationsError} /> - - } - {nodeSelector !== null && +
+ )} + {nodeSelector !== null && ( Node Selector { setError={setNodeSelectorError} /> - } - - - -
- )} + )} + + + + + )} ); }; diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/KeyPairEdit.tsx b/portal-ui/src/screens/Console/Tenants/TenantDetails/KeyPairEdit.tsx index 9ea9e1413..e630ace9a 100644 --- a/portal-ui/src/screens/Console/Tenants/TenantDetails/KeyPairEdit.tsx +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/KeyPairEdit.tsx @@ -82,73 +82,72 @@ const KeyPairEdit = ({ let keyValueInputs = newValues.map((_, index) => { return ( - - - -
- { - 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()}`] || ""} - /> - { - 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()}`] || ""} - /> - - { + + +
+ { let tempLabels = [...newValues]; - tempLabels.push({ key: "", value: "" }); + tempLabels[index].key = e.target.value; setNewValues(tempLabels); + cleanValidation(`key-${index.toString()}`); }} - > - - - - - { - if (newValues.length === 1) { - setNewValues([{ key: "", value: "" }]); - } - if (newValues.length > 1) { + index={index} + key={`csv-key-${index.toString()}`} + error={error[`key-${index.toString()}`] || ""} + /> + { + 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()}`] || ""} + /> + + { let tempLabels = [...newValues]; - tempLabels.splice(index, 1); + tempLabels.push({ key: "", value: "" }); setNewValues(tempLabels); - } - }} - > - - - -
+ }} + > + +
+
+ + { + if (newValues.length === 1) { + setNewValues([{ key: "", value: "" }]); + } + if (newValues.length > 1) { + let tempLabels = [...newValues]; + tempLabels.splice(index, 1); + setNewValues(tempLabels); + } + }} + > + + + +
); diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantDetails.tsx b/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantDetails.tsx index d261089ff..3b10ca493 100644 --- a/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantDetails.tsx +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantDetails.tsx @@ -391,7 +391,10 @@ const TenantDetails = ({ classes }: ITenantDetailsProps) => { } /> } /> } /> - } /> + } + /> } /> } /> } /> diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/tenantMonitoringSlice.ts b/portal-ui/src/screens/Console/Tenants/TenantDetails/tenantMonitoringSlice.ts index 7b6f59a3a..c956151c0 100644 --- a/portal-ui/src/screens/Console/Tenants/TenantDetails/tenantMonitoringSlice.ts +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/tenantMonitoringSlice.ts @@ -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) => { - state.prometheusEnabled = action.payload; - }, - setImage: (state, action: PayloadAction) => { - state.image = action.payload; - }, - setSidecarImage:(state, action: PayloadAction) => { - state.sidecarImage = action.payload; - }, - setInitImage: (state, action: PayloadAction) => { - state.initImage = action.payload; - }, - setStorageClassName: (state, action: PayloadAction) => { - state.storageClassName = action.payload; - }, - setLabels: (state, action: PayloadAction) => { - state.labels = action.payload; - }, - setAnnotations: (state, action: PayloadAction) => { - state.annotations = action.payload; - }, - setNodeSelector: (state, action: PayloadAction) => { - state.nodeSelector = action.payload; - }, - setDiskCapacityGB: (state, action: PayloadAction) => { - state.diskCapacityGB = action.payload; - }, - setServiceAccountName: (state, action: PayloadAction) => { - state.serviceAccountName = action.payload; - }, - setCPURequest: (state, action: PayloadAction) => { - state.monitoringCPURequest = action.payload; - }, - setMemRequest: (state, action: PayloadAction) => { - state.monitoringMemRequest = action.payload; - }, + name: "editTenantMonitoring", + initialState, + reducers: { + setPrometheusEnabled: (state, action: PayloadAction) => { + state.prometheusEnabled = action.payload; }, + setImage: (state, action: PayloadAction) => { + state.image = action.payload; + }, + setSidecarImage: (state, action: PayloadAction) => { + state.sidecarImage = action.payload; + }, + setInitImage: (state, action: PayloadAction) => { + state.initImage = action.payload; + }, + setStorageClassName: (state, action: PayloadAction) => { + state.storageClassName = action.payload; + }, + setLabels: (state, action: PayloadAction) => { + state.labels = action.payload; + }, + setAnnotations: (state, action: PayloadAction) => { + state.annotations = action.payload; + }, + setNodeSelector: (state, action: PayloadAction) => { + state.nodeSelector = action.payload; + }, + setDiskCapacityGB: (state, action: PayloadAction) => { + state.diskCapacityGB = action.payload; + }, + setServiceAccountName: (state, action: PayloadAction) => { + state.serviceAccountName = action.payload; + }, + setCPURequest: (state, action: PayloadAction) => { + state.monitoringCPURequest = action.payload; + }, + setMemRequest: (state, action: PayloadAction) => { + 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; \ No newline at end of file +export default editTenantMonitoringSlice.reducer; diff --git a/portal-ui/src/store.ts b/portal-ui/src/store.ts index 0b9571efa..5d3f57d30 100644 --- a/portal-ui/src/store.ts +++ b/portal-ui/src/store.ts @@ -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, diff --git a/restapi/embedded_spec.go b/restapi/embedded_spec.go index d6d783b73..6ccac17f8 100644 --- a/restapi/embedded_spec.go +++ b/restapi/embedded_spec.go @@ -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": { diff --git a/restapi/operations/object/download_object_parameters.go b/restapi/operations/object/download_object_parameters.go index 68a01981f..2f48a8a66 100644 --- a/restapi/operations/object/download_object_parameters.go +++ b/restapi/operations/object/download_object_parameters.go @@ -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 { diff --git a/restapi/operations/object/download_object_urlbuilder.go b/restapi/operations/object/download_object_urlbuilder.go index 02a70800d..c066413b1 100644 --- a/restapi/operations/object/download_object_urlbuilder.go +++ b/restapi/operations/object/download_object_urlbuilder.go @@ -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) diff --git a/restapi/user_objects.go b/restapi/user_objects.go index 194b69a5e..9af5eab95 100644 --- a/restapi/user_objects.go +++ b/restapi/user_objects.go @@ -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 diff --git a/swagger-console.yml b/swagger-console.yml index aa397e416..24ca4ece4 100644 --- a/swagger-console.yml +++ b/swagger-console.yml @@ -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.