Compare commits

..

11 Commits

Author SHA1 Message Date
Daniel Valdivia
aba7a9e1c9 Support to stream video (#1304)
Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
2021-12-07 21:36:50 -06:00
adfost
3db22a2479 Rewind mode list directory bug fix (#1297)
* rewind bug fix

* adding constant
2021-12-07 18:01:44 -08:00
Daniel Valdivia
884321cfce Fix Capitalization on Tools Screen (#1305)
Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
2021-12-07 17:41:18 -06:00
Daniel Valdivia
20c07a22e3 Remove Rewind Print (#1303)
Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
2021-12-07 15:51:01 -06:00
Alex
bf126d3a84 Added fallback to default dashboard in case Prometheus is not accesible (#1302) 2021-12-07 13:39:50 -08:00
Daniel Valdivia
1e59f131e8 Fix 1299: Tools menu not showing for Heal Only Policy (#1301)
* Fix 1299: Tools menu not showing for Heal Only Policy

* Fix caching issue

Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
2021-12-07 15:38:36 -06:00
Prakash Senthil Vel
dfcd49bb5d UX make all delete modals consistent (#1289) 2021-12-07 14:58:38 -06:00
adfost
a7ab26c81e Disallow folders to share the same name as existing files. (#1279) 2021-12-07 14:33:30 -06:00
Alex
35855daa12 Added reset configuration option to settings pages (#1292)
Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2021-12-07 10:41:52 -08:00
Lenin Alevski
3ce0b3d633 Fixed share/download object regression (#1296)
* Fixed share/download object regression
* Adding tests for computeObjectURLWithoutEncode function

Signed-off-by: Lenin Alevski <alevsk.8772@gmail.com>
2021-12-06 15:49:13 -06:00
Alex
569d2390b9 Added option to download selected items in object browser (#1286)
Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
2021-12-03 13:10:32 -06:00
59 changed files with 2194 additions and 1558 deletions

View File

@@ -42,6 +42,9 @@ type AdminInfoResponse struct {
// objects
Objects int64 `json:"objects,omitempty"`
// prometheus not ready
PrometheusNotReady bool `json:"prometheusNotReady,omitempty"`
// servers
Servers []*ServerProperties `json:"servers"`

View File

@@ -122,6 +122,9 @@ export const IAM_SCOPES = {
ADMIN_DELETE_POLICY: "admin:DeletePolicy",
ADMIN_ATTACH_USER_OR_GROUP_POLICY: "admin:AttachUserOrGroupPolicy",
ADMIN_HEAL_ACTION: "admin:Heal",
ADMIN_HEALTH_ACTION: "admin:OBDInfo",
ADMIN_CONSOLE_LOG_ACTION: "admin:ConsoleLog",
ADMIN_TRACE_ACTION: "admin:ServerTrace",
S3_ALL_ACTIONS: "s3:*",
ADMIN_ALL_ACTIONS: "admin:*",
};

View File

@@ -472,7 +472,12 @@ export const getTimeFromTimestamp = (
timestamp: string,
fullDate: boolean = false
) => {
const dateObject = new Date(parseInt(timestamp) * 1000);
const timestampToInt = parseInt(timestamp);
if (isNaN(timestampToInt)) {
return "";
}
const dateObject = new Date(timestampToInt * 1000);
if (fullDate) {
return `${dateObject.getFullYear()}-${String(

View File

@@ -1,44 +0,0 @@
// 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 * as React from "react";
import { SVGProps } from "react";
const DiagnosticIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
className={`min-icon`}
fill={"currentcolor"}
viewBox="0 0 256 256"
{...props}
>
<defs>
<clipPath id="prefix__a">
<path d="M0 0h256v256H0z" />
</clipPath>
</defs>
<g clipPath="url(#prefix__a)">
<path fill="none" d="M0 0h256v256H0z" />
<path
data-name="Uni\xF3n 17"
d="M.449 128.494A128.188 128.188 0 0 1 128.494.45h10.6v52.857a76.1 76.1 0 0 1 46.531 25.151 75.572 75.572 0 0 1 13.854 22.845 75.251 75.251 0 0 1 5.039 27.189 76.11 76.11 0 0 1-76.023 76.022 76.1 76.1 0 0 1-76.012-76.022 75.291 75.291 0 0 1 5.037-27.189 75.678 75.678 0 0 1 13.85-22.845 76.135 76.135 0 0 1 46.555-25.151v-31.18a106.369 106.369 0 0 0-19.6 3.814 106.378 106.378 0 0 0-18.193 7.25 107.579 107.579 0 0 0-16.385 10.312A108.253 108.253 0 0 0 49.54 56.524a108.229 108.229 0 0 0-11.676 15.37 107.348 107.348 0 0 0-8.787 17.356 106.17 106.17 0 0 0-7.459 39.244 107.008 107.008 0 0 0 106.877 106.892 107.017 107.017 0 0 0 106.9-106.892 10.5 10.5 0 0 1 3.1-7.479 10.49 10.49 0 0 1 7.475-3.1 10.593 10.593 0 0 1 10.584 10.58 128.2 128.2 0 0 1-128.057 128.057A128.2 128.2 0 0 1 .449 128.494Zm99.967-47.048a55.106 55.106 0 0 0-14.062 12.016 54.643 54.643 0 0 0-9.336 16.083 54.492 54.492 0 0 0-3.379 18.95 54.464 54.464 0 0 0 4.316 21.333 54.924 54.924 0 0 0 5.068 9.317 55.648 55.648 0 0 0 6.7 8.12 55.546 55.546 0 0 0 8.125 6.7 54.955 54.955 0 0 0 9.316 5.068 54.353 54.353 0 0 0 21.328 4.316 54.917 54.917 0 0 0 54.854-54.857 54.492 54.492 0 0 0-3.379-18.95 54.614 54.614 0 0 0-9.326-16.083 55.144 55.144 0 0 0-14.049-12.016 54.571 54.571 0 0 0-17.5-6.723v30.482a25.816 25.816 0 0 1 10.824 9.254 25.366 25.366 0 0 1 4.211 14.035 25.433 25.433 0 0 1-2.014 9.982 25.524 25.524 0 0 1-5.494 8.145 25.5 25.5 0 0 1-8.145 5.493 25.518 25.518 0 0 1-9.982 2.015 25.477 25.477 0 0 1-9.973-2.015 25.621 25.621 0 0 1-8.148-5.493 25.538 25.538 0 0 1-5.488-8.145 25.522 25.522 0 0 1-2.016-9.982 25.393 25.393 0 0 1 4.207-14.035 25.82 25.82 0 0 1 10.848-9.254V74.72a54.537 54.537 0 0 0-17.508 6.73Z"
/>
<path data-name="Rect\xE1ngulo 878" fill="none" d="M0 0h256v256H0z" />
</g>
</svg>
);
export default DiagnosticIcon;

View File

@@ -19,17 +19,25 @@ import React, { SVGProps } from "react";
const DiagnosticsIcon = (props: SVGProps<SVGSVGElement>) => {
return (
<svg
{...props}
xmlns="http://www.w3.org/2000/svg"
className={`min-icon`}
fill={"currentcolor"}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 10.405 10.405"
viewBox="0 0 256 256"
{...props}
>
<path
d="M9.932 4.778a.424.424 0 00-.424.424 4.31 4.31 0 01-4.305 4.3 4.309 4.309 0 01-4.3-4.3A4.311 4.311 0 014.784.918v1.259A3.06 3.06 0 002.15 5.202 3.062 3.062 0 005.208 8.26a3.062 3.062 0 003.058-3.058 3.06 3.06 0 00-2.634-3.025V.049h-.424A5.158 5.158 0 00.055 5.201a5.158 5.158 0 005.153 5.153 5.158 5.158 0 005.153-5.153.424.424 0 00-.429-.423zm-2.519.424a2.213 2.213 0 01-2.21 2.21 2.213 2.213 0 01-2.21-2.21A2.213 2.213 0 014.78 3.035v1.231a1.028 1.028 0 00-.606.936 1.03 1.03 0 001.03 1.03 1.03 1.03 0 001.03-1.03 1.028 1.028 0 00-.605-.936V3.035a2.212 2.212 0 011.784 2.167z"
stroke="#000"
strokeWidth={0.1}
/>
<defs>
<clipPath id="prefix__a">
<path d="M0 0h256v256H0z" />
</clipPath>
</defs>
<g clipPath="url(#prefix__a)">
<path fill="none" d="M0 0h256v256H0z" />
<path
data-name="Uni\xF3n 17"
d="M.449 128.494A128.188 128.188 0 0 1 128.494.45h10.6v52.857a76.1 76.1 0 0 1 46.531 25.151 75.572 75.572 0 0 1 13.854 22.845 75.251 75.251 0 0 1 5.039 27.189 76.11 76.11 0 0 1-76.023 76.022 76.1 76.1 0 0 1-76.012-76.022 75.291 75.291 0 0 1 5.037-27.189 75.678 75.678 0 0 1 13.85-22.845 76.135 76.135 0 0 1 46.555-25.151v-31.18a106.369 106.369 0 0 0-19.6 3.814 106.378 106.378 0 0 0-18.193 7.25 107.579 107.579 0 0 0-16.385 10.312A108.253 108.253 0 0 0 49.54 56.524a108.229 108.229 0 0 0-11.676 15.37 107.348 107.348 0 0 0-8.787 17.356 106.17 106.17 0 0 0-7.459 39.244 107.008 107.008 0 0 0 106.877 106.892 107.017 107.017 0 0 0 106.9-106.892 10.5 10.5 0 0 1 3.1-7.479 10.49 10.49 0 0 1 7.475-3.1 10.593 10.593 0 0 1 10.584 10.58 128.2 128.2 0 0 1-128.057 128.057A128.2 128.2 0 0 1 .449 128.494Zm99.967-47.048a55.106 55.106 0 0 0-14.062 12.016 54.643 54.643 0 0 0-9.336 16.083 54.492 54.492 0 0 0-3.379 18.95 54.464 54.464 0 0 0 4.316 21.333 54.924 54.924 0 0 0 5.068 9.317 55.648 55.648 0 0 0 6.7 8.12 55.546 55.546 0 0 0 8.125 6.7 54.955 54.955 0 0 0 9.316 5.068 54.353 54.353 0 0 0 21.328 4.316 54.917 54.917 0 0 0 54.854-54.857 54.492 54.492 0 0 0-3.379-18.95 54.614 54.614 0 0 0-9.326-16.083 55.144 55.144 0 0 0-14.049-12.016 54.571 54.571 0 0 0-17.5-6.723v30.482a25.816 25.816 0 0 1 10.824 9.254 25.366 25.366 0 0 1 4.211 14.035 25.433 25.433 0 0 1-2.014 9.982 25.524 25.524 0 0 1-5.494 8.145 25.5 25.5 0 0 1-8.145 5.493 25.518 25.518 0 0 1-9.982 2.015 25.477 25.477 0 0 1-9.973-2.015 25.621 25.621 0 0 1-8.148-5.493 25.538 25.538 0 0 1-5.488-8.145 25.522 25.522 0 0 1-2.016-9.982 25.393 25.393 0 0 1 4.207-14.035 25.82 25.82 0 0 1 10.848-9.254V74.72a54.537 54.537 0 0 0-17.508 6.73Z"
/>
<path data-name="Rect\xE1ngulo 878" fill="none" d="M0 0h256v256H0z" />
</g>
</svg>
);
};

View File

@@ -42,11 +42,11 @@ const UploadFile = (props: SVGProps<SVGSVGElement>) => {
<g transform="translate(-0.036 -24.789)">
<path d="M239.185,72.637A29.456,29.456,0,0,0,209.767,43.6H128.581l-1.119-1.512c-5.078-6.886-12.756-17.3-26.1-17.3H49.394A29.455,29.455,0,0,0,19.972,54.21a19.778,19.778,0,0,0,.236,3.081V70.763A29.818,29.818,0,0,0,.036,98.947c0,.6.023,1.205.076,1.806L9.8,207.577A29.8,29.8,0,0,0,39.545,236.2h175.73A29.8,29.8,0,0,0,245.021,207.6L254.947,100.8q.088-.928.09-1.852A29.792,29.792,0,0,0,239.185,72.637ZM49.394,44.808h51.963c6.586,0,13.645,18.813,20.7,18.813h87.709a9.429,9.429,0,0,1,9.4,9.4v4.7H40.213V54.206h-.229A9.431,9.431,0,0,1,49.394,44.808ZM225.031,206.43a9.781,9.781,0,0,1-9.754,9.748H39.547a9.779,9.779,0,0,1-9.75-9.748L20.051,98.947A9.782,9.782,0,0,1,29.8,89.192H225.268a9.788,9.788,0,0,1,9.758,9.755Z" />
<g transform="translate(-351.512 467)">
<g transform="translate(352 -469)" clip-path="url(#a)">
<g transform="translate(352 -469)" clipPath="url(#a)">
<path d="M118.046,203.4c0,12.123,18.976,12.123,18.976,0V126.379l10.748,10.443c8.823,8.569,22.236-4.465,13.415-13.034L134.3,97.665a9.685,9.685,0,0,0-13.526,0L93.89,123.788c-8.82,8.568,4.592,21.6,13.415,13.034l10.745-10.443V203.4Z" />
</g>
</g>
<g clip-path="url(#b)">
<g clipPath="url(#b)">
<path d="M56.052,158.235c0-12.121,18.978-12.121,18.978,0v66.218H185.056V158.235c0-12.121,18.973-12.121,18.973,0v75.436a9.357,9.357,0,0,1-9.486,9.217h-129a9.357,9.357,0,0,1-9.486-9.217V158.235Zm64.5,45.162c0,12.123,18.976,12.123,18.976,0V126.379l10.748,10.443c8.823,8.569,22.236-4.465,13.415-13.034L136.8,97.665a9.685,9.685,0,0,0-13.526,0L96.394,123.788c-8.82,8.568,4.593,21.6,13.415,13.034l10.745-10.443V203.4Z" />
</g>
</g>

View File

@@ -34,7 +34,6 @@ export { default as CopyIcon } from "./CopyIcon";
export { default as CreateIcon } from "./CreateIcon";
export { default as DashboardIcon } from "./DashboardIcon";
export { default as DeleteIcon } from "./DeleteIcon";
export { default as DiagnosticIcon } from "./DiagnosticIcon";
export { default as DiagnosticsIcon } from "./DiagnosticsIcon";
export { default as DocumentationIcon } from "./DocumentationIcon";
export { default as DownloadIcon } from "./DownloadIcon";

View File

@@ -14,30 +14,19 @@
// 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, { useEffect, useState } from "react";
import React from "react";
import { connect } from "react-redux";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
} from "@mui/material";
import { DialogContentText } from "@mui/material";
import { setErrorSnackMessage } from "../../../actions";
import { ErrorResponseHandler } from "../../../common/types";
import api from "../../../common/api";
import { deleteDialogStyles } from "../Common/FormComponents/common/styleLibrary";
import IconButton from "@mui/material/IconButton";
import CloseIcon from "@mui/icons-material/Close";
import useApi from "../Common/Hooks/useApi";
import ConfirmDialog from "../Common/ModalWrapper/ConfirmDialog";
const styles = (theme: Theme) =>
createStyles({
...deleteDialogStyles,
wrapText: {
maxWidth: "200px",
whiteSpace: "normal",
@@ -60,95 +49,38 @@ const DeleteServiceAccount = ({
selectedServiceAccount,
setErrorSnackMessage,
}: IDeleteServiceAccountProps) => {
const [deleteLoading, setDeleteLoading] = useState(false);
const onDelSuccess = () => closeDeleteModalAndRefresh(true);
const onDelError = (err: ErrorResponseHandler) => setErrorSnackMessage(err);
const onClose = () => closeDeleteModalAndRefresh(false);
useEffect(() => {
if (deleteLoading) {
api
.invoke("DELETE", `/api/v1/service-accounts/${selectedServiceAccount}`)
.then(() => {
setDeleteLoading(false);
closeDeleteModalAndRefresh(true);
})
.catch((err: ErrorResponseHandler) => {
setDeleteLoading(false);
setErrorSnackMessage(err);
});
}
}, [
deleteLoading,
closeDeleteModalAndRefresh,
selectedServiceAccount,
setErrorSnackMessage,
]);
const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError);
const removeRecord = () => {
if (selectedServiceAccount === null) {
return;
}
if (!selectedServiceAccount) {
return null;
}
setDeleteLoading(true);
const onConfirmDelete = () => {
invokeDeleteApi(
"DELETE",
`/api/v1/service-accounts/${selectedServiceAccount}`
);
};
return (
<Dialog
open={deleteOpen}
classes={classes}
className={classes.root}
onClose={() => {
closeDeleteModalAndRefresh(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title" className={classes.title}>
<div className={classes.titleText}>Delete ServiceAccount</div>
<div className={classes.closeContainer}>
<IconButton
aria-label="close"
className={classes.closeButton}
onClick={() => {
closeDeleteModalAndRefresh(true);
}}
disableRipple
size="small"
>
<CloseIcon />
</IconButton>
</div>
</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
<ConfirmDialog
title={`Delete Service Account`}
confirmText={"Delete"}
isOpen={deleteOpen}
isLoading={deleteLoading}
onConfirm={onConfirmDelete}
onClose={onClose}
confirmationContent={
<DialogContentText>
Are you sure you want to delete service account{" "}
<b className={classes.wrapText}>{selectedServiceAccount}</b>?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
type="button"
variant="outlined"
onClick={() => {
closeDeleteModalAndRefresh(false);
}}
color="primary"
disabled={deleteLoading}
>
Cancel
</Button>
<Button
type="button"
variant="outlined"
onClick={removeRecord}
color="secondary"
autoFocus
disabled={deleteLoading}
>
Delete
</Button>
</DialogActions>
</Dialog>
}
/>
);
};

View File

@@ -15,23 +15,17 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
} from "@mui/material";
import { DialogContentText } from "@mui/material";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import { connect } from "react-redux";
import api from "../../../../common/api";
import { ErrorResponseHandler } from "../../../../common/types";
import { setErrorSnackMessage } from "../../../../actions";
import { AppState } from "../../../../store";
import useApi from "../../Common/Hooks/useApi";
import ConfirmDialog from "../../Common/ModalWrapper/ConfirmDialog";
const mapState = (state: AppState) => ({
session: state.console.session,
@@ -64,46 +58,31 @@ const DeleteAccessRule = ({
bucket,
toDelete,
}: IDeleteAccessRule) => {
const deleteProcess = () => {
api
.invoke("DELETE", `/api/v1/bucket/${bucket}/access-rules`, {
prefix: toDelete,
})
.then((res: any) => {})
.catch((err: ErrorResponseHandler) => {
setErrorSnackMessage(err);
});
const onDelSuccess = () => onClose();
const onDelError = (err: ErrorResponseHandler) => setErrorSnackMessage(err);
const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError);
const onConfirmDelete = () => {
invokeDeleteApi("DELETE", `/api/v1/bucket/${bucket}/access-rules`, {
prefix: toDelete,
});
};
return (
<Dialog
open={modalOpen}
<ConfirmDialog
title={`Delete Access Rule`}
confirmText={"Delete"}
isOpen={modalOpen}
isLoading={deleteLoading}
onConfirm={onConfirmDelete}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete Access Rule</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
confirmationContent={
<DialogContentText>
Are you sure you want to delete this access rule?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="primary">
Cancel
</Button>
<Button
onClick={() => {
deleteProcess();
onClose();
}}
color="secondary"
autoFocus
>
Delete
</Button>
</DialogActions>
</Dialog>
}
/>
);
};

View File

@@ -14,18 +14,10 @@
// 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 React from "react";
import get from "lodash/get";
import { connect } from "react-redux";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
} from "@mui/material";
import { DialogContentText } from "@mui/material";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
@@ -33,7 +25,8 @@ import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import { setErrorSnackMessage } from "../../../../actions";
import { AppState } from "../../../../store";
import { ErrorResponseHandler } from "../../../../common/types";
import api from "../../../../common/api";
import useApi from "../../Common/Hooks/useApi";
import ConfirmDialog from "../../Common/ModalWrapper/ConfirmDialog";
interface IDeleteBucketTagModal {
deleteOpen: boolean;
@@ -66,62 +59,45 @@ const DeleteBucketTagModal = ({
setErrorSnackMessage,
classes,
}: IDeleteBucketTagModal) => {
const [deleteLoading, setDeleteSending] = useState<boolean>(false);
const [tagKey, tagLabel] = selectedTag;
const removeTagProcess = () => {
setDeleteSending(true);
const onDelSuccess = () => onCloseAndUpdate(true);
const onDelError = (err: ErrorResponseHandler) => setErrorSnackMessage(err);
const onClose = () => onCloseAndUpdate(false);
const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError);
if (!selectedTag) {
return null;
}
const onConfirmDelete = () => {
const cleanObject = { ...currentTags };
delete cleanObject[tagKey];
api
.invoke("PUT", `/api/v1/buckets/${bucketName}/tags`, {
tags: cleanObject,
})
.then((res: any) => {
setDeleteSending(false);
onCloseAndUpdate(true);
})
.catch((error: ErrorResponseHandler) => {
setErrorSnackMessage(error);
setDeleteSending(false);
});
invokeDeleteApi("PUT", `/api/v1/buckets/${bucketName}/tags`, {
tags: cleanObject,
});
};
return (
<Dialog
open={deleteOpen}
onClose={() => {
onCloseAndUpdate(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete Tag</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
<ConfirmDialog
title={`Delete Tag`}
confirmText={"Delete"}
isOpen={deleteOpen}
isLoading={deleteLoading}
onConfirm={onConfirmDelete}
onClose={onClose}
confirmationContent={
<DialogContentText>
Are you sure you want to delete the tag{" "}
<b className={classes.wrapText}>
{tagKey} : {tagLabel}
</b>
</b>{" "}
?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
onCloseAndUpdate(false);
}}
color="primary"
disabled={deleteLoading}
>
Cancel
</Button>
<Button onClick={removeTagProcess} color="secondary" autoFocus>
Delete
</Button>
</DialogActions>
</Dialog>
}
/>
);
};

View File

@@ -14,22 +14,15 @@
// 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 React from "react";
import get from "lodash/get";
import { connect } from "react-redux";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
} from "@mui/material";
import api from "../../../../common/api";
import { BucketEvent, BucketList } from "../types";
import { DialogContentText } from "@mui/material";
import { BucketEvent } from "../types";
import { setErrorSnackMessage } from "../../../../actions";
import { ErrorResponseHandler } from "../../../../common/types";
import useApi from "../../Common/Hooks/useApi";
import ConfirmDialog from "../../Common/ModalWrapper/ConfirmDialog";
interface IDeleteEventProps {
closeDeleteModalAndRefresh: (refresh: boolean) => void;
@@ -46,78 +39,50 @@ const DeleteEvent = ({
bucketEvent,
setErrorSnackMessage,
}: IDeleteEventProps) => {
const [deleteLoading, setDeleteLoading] = useState<boolean>(false);
const onDelSuccess = () => closeDeleteModalAndRefresh(true);
const onDelError = (err: ErrorResponseHandler) => setErrorSnackMessage(err);
const onClose = () => closeDeleteModalAndRefresh(false);
const removeRecord = () => {
if (deleteLoading) {
return;
}
const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError);
if (!selectedBucket) {
return null;
}
const onConfirmDelete = () => {
if (bucketEvent === null) {
return;
}
setDeleteLoading(true);
const events = get(bucketEvent, "events", []);
const prefix = get(bucketEvent, "prefix", "");
const suffix = get(bucketEvent, "suffix", "");
api
.invoke(
"DELETE",
`/api/v1/buckets/${selectedBucket}/events/${bucketEvent.arn}`,
{
events,
prefix,
suffix,
}
)
.then((res: BucketList) => {
setDeleteLoading(false);
closeDeleteModalAndRefresh(true);
})
.catch((err: ErrorResponseHandler) => {
setDeleteLoading(false);
setErrorSnackMessage(err);
});
invokeDeleteApi(
"DELETE",
`/api/v1/buckets/${selectedBucket}/events/${bucketEvent.arn}`,
{
events,
prefix,
suffix,
}
);
};
return (
<Dialog
open={deleteOpen}
onClose={() => {
closeDeleteModalAndRefresh(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete Event</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
<ConfirmDialog
title={`Delete Event`}
confirmText={"Delete"}
isOpen={deleteOpen}
isLoading={deleteLoading}
onConfirm={onConfirmDelete}
onClose={onClose}
confirmationContent={
<DialogContentText>
Are you sure you want to delete this event?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
closeDeleteModalAndRefresh(false);
}}
color="primary"
disabled={deleteLoading}
>
Cancel
</Button>
<Button
onClick={() => {
removeRecord();
}}
color="secondary"
autoFocus
>
Delete
</Button>
</DialogActions>
</Dialog>
}
/>
);
};

View File

@@ -14,20 +14,13 @@
// 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 React from "react";
import { connect } from "react-redux";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
} from "@mui/material";
import { DialogContentText } from "@mui/material";
import { setErrorSnackMessage } from "../../../../actions";
import { ErrorResponseHandler } from "../../../../common/types";
import api from "../../../../common/api";
import useApi from "../../Common/Hooks/useApi";
import ConfirmDialog from "../../Common/ModalWrapper/ConfirmDialog";
interface IDeleteReplicationProps {
closeDeleteModalAndRefresh: (refresh: boolean) => void;
@@ -44,68 +37,40 @@ const DeleteReplicationRule = ({
ruleToDelete,
setErrorSnackMessage,
}: IDeleteReplicationProps) => {
const [deleteLoading, setDeleteLoading] = useState<boolean>(false);
const onDelSuccess = () => closeDeleteModalAndRefresh(true);
const onDelError = (err: ErrorResponseHandler) => setErrorSnackMessage(err);
const onClose = () => closeDeleteModalAndRefresh(false);
const removeRecord = () => {
if (!deleteLoading) {
setDeleteLoading(true);
const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError);
api
.invoke(
"DELETE",
`/api/v1/buckets/${selectedBucket}/replication/${ruleToDelete}`
)
.then(() => {
setDeleteLoading(false);
closeDeleteModalAndRefresh(true);
})
.catch((err: ErrorResponseHandler) => {
setDeleteLoading(false);
setErrorSnackMessage(err);
});
}
if (!selectedBucket) {
return null;
}
const onConfirmDelete = () => {
invokeDeleteApi(
"DELETE",
`/api/v1/buckets/${selectedBucket}/replication/${ruleToDelete}`
);
};
return (
<Dialog
open={deleteOpen}
onClose={() => {
closeDeleteModalAndRefresh(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete Replication Rule</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
<ConfirmDialog
title={`Delete Replication Rule`}
confirmText={"Delete"}
isOpen={deleteOpen}
isLoading={deleteLoading}
onConfirm={onConfirmDelete}
onClose={onClose}
confirmationContent={
<DialogContentText>
Are you sure you want to delete replication rule <b>{ruleToDelete}</b>
? <br />
Remember, at lease one rule must be present once replication has been
enabled
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
closeDeleteModalAndRefresh(false);
}}
color="primary"
disabled={deleteLoading}
>
Cancel
</Button>
<Button
onClick={() => {
removeRecord();
}}
color="secondary"
autoFocus
>
Delete
</Button>
</DialogActions>
</Dialog>
}
/>
);
};

View File

@@ -14,21 +14,13 @@
// 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 React from "react";
import { connect } from "react-redux";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
} from "@mui/material";
import { BucketList } from "../types";
import { DialogContentText } from "@mui/material";
import { setErrorSnackMessage } from "../../../../actions";
import { ErrorResponseHandler } from "../../../../common/types";
import api from "../../../../common/api";
import useApi from "../../Common/Hooks/useApi";
import ConfirmDialog from "../../Common/ModalWrapper/ConfirmDialog";
interface IDeleteBucketProps {
closeDeleteModalAndRefresh: (refresh: boolean) => void;
@@ -43,65 +35,37 @@ const DeleteBucket = ({
selectedBucket,
setErrorSnackMessage,
}: IDeleteBucketProps) => {
const [deleteLoading, setDeleteLoading] = useState<boolean>(false);
const onDelSuccess = () => closeDeleteModalAndRefresh(true);
const onDelError = (err: ErrorResponseHandler) => setErrorSnackMessage(err);
const onClose = () => closeDeleteModalAndRefresh(false);
const removeRecord = () => {
if (!deleteLoading) {
setDeleteLoading(true);
const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError);
api
.invoke("DELETE", `/api/v1/buckets/${selectedBucket}`, {
name: selectedBucket,
})
.then((res: BucketList) => {
setDeleteLoading(false);
closeDeleteModalAndRefresh(true);
})
.catch((err: ErrorResponseHandler) => {
setDeleteLoading(false);
setErrorSnackMessage(err);
});
}
if (!selectedBucket) {
return null;
}
const onConfirmDelete = () => {
invokeDeleteApi("DELETE", `/api/v1/buckets/${selectedBucket}`, {
name: selectedBucket,
});
};
return (
<Dialog
open={deleteOpen}
onClose={() => {
closeDeleteModalAndRefresh(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete Bucket</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
<ConfirmDialog
title={`Delete Bucket`}
confirmText={"Delete"}
isOpen={deleteOpen}
isLoading={deleteLoading}
onConfirm={onConfirmDelete}
onClose={onClose}
confirmationContent={
<DialogContentText>
Are you sure you want to delete bucket <b>{selectedBucket}</b>? <br />
A bucket can only be deleted if it's empty.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
closeDeleteModalAndRefresh(false);
}}
color="primary"
disabled={deleteLoading}
>
Cancel
</Button>
<Button
onClick={() => {
removeRecord();
}}
color="secondary"
autoFocus
>
Delete
</Button>
</DialogActions>
</Dialog>
}
/>
);
};

View File

@@ -26,6 +26,8 @@ import { connect } from "react-redux";
import { setFileModeEnabled } from "../../../../ObjectBrowser/actions";
import history from "../../../../../../history";
import { decodeFileName, encodeFileName } from "../../../../../../common/utils";
import { setModalErrorSnackMessage } from "../../../../../../actions";
import { BucketObject } from "./types";
interface ICreateFolder {
classes: any;
@@ -33,7 +35,9 @@ interface ICreateFolder {
bucketName: string;
folderName: string;
setFileModeEnabled: typeof setFileModeEnabled;
setModalErrorSnackMessage: typeof setModalErrorSnackMessage;
onClose: () => any;
existingFiles: BucketObject[];
}
const styles = (theme: Theme) =>
@@ -54,7 +58,9 @@ const CreateFolderModal = ({
bucketName,
onClose,
setFileModeEnabled,
setModalErrorSnackMessage,
classes,
existingFiles,
}: ICreateFolder) => {
const [pathUrl, setPathUrl] = useState("");
const [isFormValid, setIsFormValid] = useState<boolean>(false);
@@ -73,6 +79,15 @@ const CreateFolderModal = ({
? decodedFolderName
: `${decodedFolderName}/`;
}
const sharesName = (record: BucketObject) =>
record.name === folderPath + pathUrl;
if (existingFiles.findIndex(sharesName) !== -1) {
setModalErrorSnackMessage({
errorMessage: "Folder cannot have the same name as an existing file",
detailedError: "",
});
return;
}
const newPath = `/buckets/${bucketName}/browse/${encodeFileName(
`${folderPath}${pathUrl}`
)}/`;
@@ -138,6 +153,7 @@ const CreateFolderModal = ({
const mapDispatchToProps = {
setFileModeEnabled,
setModalErrorSnackMessage,
};
const connector = connect(null, mapDispatchToProps);

View File

@@ -14,20 +14,13 @@
// 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 React from "react";
import { connect } from "react-redux";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
} from "@mui/material";
import { DialogContentText } from "@mui/material";
import { setErrorSnackMessage } from "../../../../../../actions";
import { ErrorResponseHandler } from "../../../../../../common/types";
import api from "../../../../../../common/api";
import useApi from "../../../../Common/Hooks/useApi";
import ConfirmDialog from "../../../../Common/ModalWrapper/ConfirmDialog";
interface IDeleteObjectProps {
closeDeleteModalAndRefresh: (refresh: boolean) => void;
@@ -44,12 +37,16 @@ const DeleteObject = ({
selectedObjects,
setErrorSnackMessage,
}: IDeleteObjectProps) => {
const [deleteLoading, setDeleteLoading] = useState<boolean>(false);
const onDelSuccess = () => closeDeleteModalAndRefresh(true);
const onDelError = (err: ErrorResponseHandler) => setErrorSnackMessage(err);
const onClose = () => closeDeleteModalAndRefresh(false);
const removeRecord = () => {
if (deleteLoading) {
return;
}
const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError);
if (!selectedObjects) {
return null;
}
const onConfirmDelete = () => {
let toSend = [];
for (let i = 0; i < selectedObjects.length; i++) {
if (selectedObjects[i].endsWith("/")) {
@@ -66,60 +63,31 @@ const DeleteObject = ({
});
}
}
setDeleteLoading(true);
api
.invoke(
if (toSend) {
invokeDeleteApi(
"POST",
`/api/v1/buckets/${selectedBucket}/delete-objects`,
toSend
)
.then(() => {
setDeleteLoading(false);
closeDeleteModalAndRefresh(true);
})
.catch((err: ErrorResponseHandler) => {
setDeleteLoading(false);
setErrorSnackMessage(err);
});
);
}
};
return (
<Dialog
open={deleteOpen}
onClose={() => {
closeDeleteModalAndRefresh(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete the selected objects?{" "}
<ConfirmDialog
title={`Delete Objects`}
confirmText={"Delete"}
isOpen={deleteOpen}
isLoading={deleteLoading}
onConfirm={onConfirmDelete}
onClose={onClose}
confirmationContent={
<DialogContentText>
Are you sure you want to delete the selected {selectedObjects.length}{" "}
objects?{" "}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
closeDeleteModalAndRefresh(false);
}}
color="primary"
disabled={deleteLoading}
>
Cancel
</Button>
<Button
onClick={() => {
removeRecord();
}}
color="secondary"
disabled={deleteLoading}
>
Delete
</Button>
</DialogActions>
</Dialog>
}
/>
);
};

View File

@@ -14,21 +14,14 @@
// 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 React from "react";
import { connect } from "react-redux";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
} from "@mui/material";
import { DialogContentText } from "@mui/material";
import { setErrorSnackMessage } from "../../../../../../actions";
import { ErrorResponseHandler } from "../../../../../../common/types";
import api from "../../../../../../common/api";
import { decodeFileName } from "../../../../../../common/utils";
import ConfirmDialog from "../../../../Common/ModalWrapper/ConfirmDialog";
import useApi from "../../../../Common/Hooks/useApi";
interface IDeleteObjectProps {
closeDeleteModalAndRefresh: (refresh: boolean) => void;
@@ -45,67 +38,39 @@ const DeleteObject = ({
selectedObject,
setErrorSnackMessage,
}: IDeleteObjectProps) => {
const [deleteLoading, setDeleteLoading] = useState<boolean>(false);
const onDelSuccess = () => closeDeleteModalAndRefresh(true);
const onDelError = (err: ErrorResponseHandler) => setErrorSnackMessage(err);
const onClose = () => closeDeleteModalAndRefresh(false);
const removeRecord = () => {
if (deleteLoading) {
return;
}
const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError);
if (!selectedObject) {
return null;
}
const onConfirmDelete = () => {
const decodedSelectedObject = decodeFileName(selectedObject);
const recursive = decodedSelectedObject.endsWith("/");
api
.invoke(
"DELETE",
`/api/v1/buckets/${selectedBucket}/objects?path=${selectedObject}&recursive=${recursive}`
)
.then(() => {
setDeleteLoading(false);
closeDeleteModalAndRefresh(true);
})
.catch((err: ErrorResponseHandler) => {
setDeleteLoading(false);
setErrorSnackMessage(err);
});
invokeDeleteApi(
"DELETE",
`/api/v1/buckets/${selectedBucket}/objects?path=${selectedObject}&recursive=${recursive}`
);
};
return (
<Dialog
open={deleteOpen}
onClose={() => {
closeDeleteModalAndRefresh(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
<ConfirmDialog
title={`Delete Object`}
confirmText={"Delete"}
isOpen={deleteOpen}
isLoading={deleteLoading}
onConfirm={onConfirmDelete}
onClose={onClose}
confirmationContent={
<DialogContentText>
Are you sure you want to delete:{" "}
<b>{decodeFileName(selectedObject)}</b>?{" "}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
closeDeleteModalAndRefresh(false);
}}
color="primary"
disabled={deleteLoading}
>
Cancel
</Button>
<Button
onClick={() => {
removeRecord();
}}
color="secondary"
disabled={deleteLoading}
>
Delete
</Button>
</DialogActions>
</Dialog>
}
/>
);
};

View File

@@ -79,7 +79,7 @@ import SearchBox from "../../../../Common/SearchBox";
import withSuspense from "../../../../Common/Components/withSuspense";
import { displayName } from "./utils";
import UploadFolderIcon from "../../../../../../icons/UploadFolderIcon";
import { UploadFolderIcon, DownloadIcon } from "../../../../../../icons";
const AddFolderIcon = React.lazy(
() => import("../../../../../../icons/AddFolderIcon")
@@ -490,7 +490,6 @@ const ListObjects = ({
? decodedPath
: decodedPath + "/";
}
api
.invoke(
"GET",
@@ -531,10 +530,23 @@ const ListObjects = ({
setFileModeEnabled(false);
setLoading(false);
} else {
// This is an empty folder.
// This code prevents the program from opening a file when a substring of that file is entered as a new folder.
// Previously, if there was a file test1.txt and the folder test was created with the same prefix, the program
// would open test1.txt instead
let found = false;
let pathPrefixChopped = pathPrefix.slice(
0,
pathPrefix.length - 1
);
for (let i = 0; i < res.objects.length; i++) {
if (res.objects[i].name === pathPrefixChopped) {
found = true;
}
}
if (
res.objects.length === 1 &&
res.objects[0].name.endsWith("/")
(res.objects.length === 1 &&
res.objects[0].name.endsWith("/")) ||
!found
) {
setFileModeEnabled(false);
} else {
@@ -661,7 +673,7 @@ const ListObjects = ({
uploadUrl = `${uploadUrl}?prefix=${encodedPath}`;
}
const identity = btoa(
const identity = encodeFileName(
`${bucketName}-${encodedPath}-${new Date().getTime()}-${Math.random()}`
);
@@ -753,8 +765,8 @@ const ListObjects = ({
return state ? "Yes" : "No";
};
const downloadObject = (object: BucketObject) => {
const identityDownload = btoa(
const downloadObject = (object: BucketObject | RewindObject) => {
const identityDownload = encodeFileName(
`${bucketName}-${object.name}-${new Date().getTime()}-${Math.random()}`
);
@@ -990,6 +1002,25 @@ const ListObjects = ({
setSelectedObjects(elements);
};
const downloadSelected = () => {
if (selectedObjects.length !== 0) {
let itemsToDownload: BucketObject[] | RewindObject[] = [];
const filterFunction = (currValue: BucketObject | RewindObject) =>
selectedObjects.includes(currValue.name);
if (rewindEnabled) {
itemsToDownload = rewind.filter(filterFunction);
} else {
itemsToDownload = filteredRecords.filter(filterFunction);
}
itemsToDownload.forEach((filteredItem) => {
downloadObject(filteredItem);
});
}
};
return (
<React.Fragment>
{shareFileModalOpen && selectedPreview && (
@@ -1026,6 +1057,7 @@ const ListObjects = ({
bucketName={bucketName}
folderName={internalPaths}
onClose={closeAddFolderModal}
existingFiles={records}
/>
)}
{rewindSelect && (
@@ -1175,22 +1207,33 @@ const ListObjects = ({
placeholder="Search Objects"
/>
</SecureComponent>
<SecureComponent
scopes={[IAM_SCOPES.S3_DELETE_OBJECT]}
resource={bucketName}
>
<div>
<Button
variant="contained"
color="primary"
endIcon={<DeleteIcon />}
onClick={() => {
setDeleteMultipleOpen(true);
}}
endIcon={<DownloadIcon />}
onClick={downloadSelected}
disabled={selectedObjects.length === 0}
>
Delete Selected
Download Selected
</Button>
</SecureComponent>
<SecureComponent
scopes={[IAM_SCOPES.S3_DELETE_OBJECT]}
resource={bucketName}
>
<Button
variant="contained"
color="primary"
endIcon={<DeleteIcon />}
onClick={() => {
setDeleteMultipleOpen(true);
}}
disabled={selectedObjects.length === 0}
>
Delete Selected
</Button>
</SecureComponent>
</div>
</Grid>
<Grid item xs={12}>
<br />

View File

@@ -14,27 +14,19 @@
// 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 React from "react";
import get from "lodash/get";
import { connect } from "react-redux";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
} from "@mui/material";
import { DialogContentText } from "@mui/material";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { modalBasic } from "../../../../Common/FormComponents/common/styleLibrary";
import { setErrorSnackMessage } from "../../../../../../actions";
import { AppState } from "../../../../../../store";
import { ErrorResponseHandler } from "../../../../../../common/types";
import api from "../../../../../../common/api";
import { encodeFileName } from "../../../../../../common/utils";
import useApi from "../../../../Common/Hooks/useApi";
import ConfirmDialog from "../../../../Common/ModalWrapper/ConfirmDialog";
interface IDeleteTagModal {
deleteOpen: boolean;
@@ -58,7 +50,6 @@ const styles = (theme: Theme) =>
marginTop: 0,
marginBottom: 32,
},
...modalBasic,
});
const DeleteTagModal = ({
@@ -73,69 +64,51 @@ const DeleteTagModal = ({
setErrorSnackMessage,
classes,
}: IDeleteTagModal) => {
const [deleteLoading, setDeleteSending] = useState<boolean>(false);
const [tagKey, tagLabel] = selectedTag;
const removeTagProcess = () => {
setDeleteSending(true);
const onDelSuccess = () => onCloseAndUpdate(true);
const onDelError = (err: ErrorResponseHandler) => setErrorSnackMessage(err);
const onClose = () => onCloseAndUpdate(false);
const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError);
if (!selectedTag) {
return null;
}
const onConfirmDelete = () => {
const cleanObject = { ...currentTags };
delete cleanObject[tagKey];
const verID = distributedSetup ? versionId : "null";
api
.invoke(
"PUT",
`/api/v1/buckets/${bucketName}/objects/tags?prefix=${encodeFileName(
selectedObject
)}&version_id=${verID}`,
{ tags: cleanObject }
)
.then((res: any) => {
setDeleteSending(false);
onCloseAndUpdate(true);
})
.catch((error: ErrorResponseHandler) => {
setErrorSnackMessage(error);
setDeleteSending(false);
});
invokeDeleteApi(
"PUT",
`/api/v1/buckets/${bucketName}/objects/tags?prefix=${encodeFileName(
selectedObject
)}&version_id=${verID}`,
{ tags: cleanObject }
);
};
return (
<Dialog
open={deleteOpen}
onClose={() => {
onCloseAndUpdate(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete Tag</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
<ConfirmDialog
title={`Delete Tag`}
confirmText={"Delete"}
isOpen={deleteOpen}
isLoading={deleteLoading}
onConfirm={onConfirmDelete}
onClose={onClose}
confirmationContent={
<DialogContentText>
Are you sure you want to delete the tag{" "}
<b className={classes.wrapText}>
{tagKey} : {tagLabel}
</b>{" "}
from {selectedObject}?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
onCloseAndUpdate(false);
}}
color="primary"
disabled={deleteLoading}
>
Cancel
</Button>
<Button onClick={removeTagProcess} color="secondary" autoFocus>
Delete
</Button>
</DialogActions>
</Dialog>
}
/>
);
};

View File

@@ -391,7 +391,7 @@ const ObjectDetails = ({
};
const downloadObject = (object: IFileInfo) => {
const identityDownload = btoa(
const identityDownload = encodeFileName(
`${bucketName}-${object.name}-${new Date().getTime()}-${Math.random()}`
);

View File

@@ -94,19 +94,55 @@ const PreviewFile = ({
<LinearProgress />
</Grid>
)}
<div className={`${loading ? classes.iframeHidden : ""} iframeBase`}>
<iframe
src={path}
title="File Preview"
allowTransparency
className={`${classes.iframeContainer} ${
isFullscreen ? "fullHeight" : objectType
}`}
onLoad={iframeLoaded}
{objectType === "video" && (
<video
style={{ width: "100%", height: "auto" }}
autoPlay={true}
controls={true}
muted={false}
playsInline={true}
onPlay={iframeLoaded}
>
File couldn't be loaded. Please try Download instead
</iframe>
</div>
<source src={path} type="video/mp4" />
</video>
)}
{objectType === "audio" && (
<audio
style={{ width: "100%", height: "auto" }}
autoPlay={true}
controls={true}
muted={false}
playsInline={true}
onPlay={iframeLoaded}
>
<source src={path} type="audio/mpeg" />
</audio>
)}
{objectType === "image" && (
<img
style={{ width: "100%", height: "auto" }}
src={path}
alt={"preview"}
onLoad={iframeLoaded}
/>
)}
{objectType !== "video" &&
objectType !== "audio" &&
objectType !== "image" && (
<div className={`${loading ? classes.iframeHidden : ""} iframeBase`}>
<iframe
src={path}
title="File Preview"
allowTransparency
className={`${classes.iframeContainer} ${
isFullscreen ? "fullHeight" : objectType
}`}
onLoad={iframeLoaded}
>
File couldn't be loaded. Please try Download instead
</iframe>
</div>
)}
</Fragment>
);
};

View File

@@ -0,0 +1,32 @@
import { useState } from "react";
import api from "../../../../common/api";
import { ErrorResponseHandler } from "../../../../common/types";
type NoReturnFunction = (param?: any) => void;
type ApiMethodToInvoke = (method: string, url: string, data?: any) => void;
type IsApiInProgress = boolean;
const useApi = (
onSuccess: NoReturnFunction,
onError: NoReturnFunction
): [IsApiInProgress, ApiMethodToInvoke] => {
const [isLoading, setIsLoading] = useState<boolean>(false);
const callApi = (method: string, url: string, data?: any) => {
setIsLoading(true);
api
.invoke(method, url, data)
.then((res: any) => {
setIsLoading(false);
onSuccess(res);
})
.catch((err: ErrorResponseHandler) => {
setIsLoading(false);
onError(err);
});
};
return [isLoading, callApi];
};
export default useApi;

View File

@@ -41,7 +41,6 @@ import {
CreateIcon,
DashboardIcon,
DeleteIcon,
DiagnosticIcon,
DiagnosticsIcon,
DocumentationIcon,
DownloadIcon,
@@ -225,10 +224,6 @@ const IconsScreen = ({ classes }: IIconsScreenSimple) => {
<DeleteIcon /> <br />
DeleteIcon
</Grid>
<Grid item xs={3} sm={2} md={1}>
<DiagnosticIcon /> <br />
DiagnosticIcon
</Grid>
<Grid item xs={3} sm={2} md={1}>
<DiagnosticsIcon /> <br />
DiagnosticsIcon

View File

@@ -0,0 +1,117 @@
import React from "react";
import {
Button,
ButtonProps,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
} from "@mui/material";
import { LoadingButton } from "@mui/lab";
import IconButton from "@mui/material/IconButton";
import CloseIcon from "@mui/icons-material/Close";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { deleteDialogStyles } from "../FormComponents/common/styleLibrary";
const styles = (theme: Theme) =>
createStyles({
...deleteDialogStyles,
});
type ConfirmDialogProps = {
isOpen?: boolean;
onClose: () => void;
onCancel?: () => void;
onConfirm: () => void;
classes?: any;
title: string;
isLoading?: boolean;
confirmationContent: React.ReactNode | React.ReactNode[];
cancelText?: string;
confirmText?: string;
confirmButtonProps?: Partial<ButtonProps>;
cancelButtonProps?: Partial<ButtonProps>;
};
const ConfirmDialog = ({
isOpen = false,
onClose,
onCancel,
onConfirm,
classes = {},
title = "",
isLoading,
confirmationContent,
cancelText = "Cancel",
confirmText = "Confirm",
confirmButtonProps = {},
cancelButtonProps = {},
}: ConfirmDialogProps) => {
return (
<Dialog
open={isOpen}
classes={classes}
onClose={(event, reason) => {
if (reason !== "backdropClick") {
onClose(); // close on Esc but not on click outside
}
}}
className={classes.root}
onBackdropClick={() => {
return false;
}}
>
<DialogTitle className={classes.title}>
<div className={classes.titleText}>{title}</div>
<div className={classes.closeContainer}>
<IconButton
aria-label="close"
className={classes.closeButton}
onClick={onClose}
disableRipple
size="small"
>
<CloseIcon />
</IconButton>
</div>
</DialogTitle>
<DialogContent className={classes.content}>
{confirmationContent}
</DialogContent>
<DialogActions className={classes.actions}>
<Button
className={classes.cancelButton}
onClick={onCancel || onClose}
disabled={isLoading}
type="button"
{...cancelButtonProps}
variant="outlined"
color="primary"
>
{cancelText}
</Button>
<LoadingButton
className={classes.confirmButton}
type="button"
onClick={onConfirm}
loading={isLoading}
disabled={isLoading}
{...confirmButtonProps}
variant="outlined"
color="secondary"
loadingPosition="start"
startIcon={null}
autoFocus
>
{confirmText}
</LoadingButton>
</DialogActions>
</Dialog>
);
};
export default withStyles(styles)(ConfirmDialog);

View File

@@ -270,6 +270,9 @@ const Console = ({
const allowedPages = !session
? []
: session.pages.reduce((result: any, item: any, index: any) => {
if (item.startsWith("/tools")) {
result["/tools"] = true;
}
result[item] = true;
return result;
}, {});

View File

@@ -133,6 +133,31 @@ const BasicDashboard = ({ classes, usage }: IDashboardProps) => {
return (
<Fragment>
<div className={classes.dashboardBG} />
{usage?.prometheusNotReady && (
<Grid
container
justifyContent={"center"}
alignContent={"center"}
alignItems={"center"}
>
<Grid item xs={8}>
<HelpBox
iconComponent={<PrometheusIcon />}
title={"We can't retrieve advanced metrics at this time"}
help={
<Fragment>
MinIO Dashboard will display basic metrics as we couldn't
connect to Prometheus successfully.
<br /> <br />
Please try again in a few minutes. If the problem persists,
you can review your configuration and confirm that Prometheus
server is up and running.
</Fragment>
}
/>
</Grid>
</Grid>
)}
<Grid container spacing={2}>
<Grid item xs={12} className={classes.generalStatusTitle}>
General Status
@@ -241,45 +266,47 @@ const BasicDashboard = ({ classes, usage }: IDashboardProps) => {
</TabPanel>
</Grid>
</Grid>
<Grid
container
justifyContent={"center"}
alignContent={"center"}
alignItems={"center"}
>
<Grid item xs={8}>
<HelpBox
iconComponent={<PrometheusIcon />}
title={"Monitoring"}
help={
<Fragment>
The MinIO Dashboard is displaying basic metrics only due to
missing the{" "}
<a
href="https://docs.min.io/minio/baremetal/console/minio-console.html?ref=con#configuration"
target="_blank"
rel="noreferrer"
>
necessary settings
</a>{" "}
for displaying extended metrics.
<br />
<br />
See{" "}
<a
href="https://docs.min.io/minio/baremetal/monitoring/metrics-alerts/collect-minio-metrics-using-prometheus.html?ref=con#minio-metrics-collect-using-prometheus"
target="_blank"
rel="noreferrer"
>
Collect MinIO Metrics Using Prometheus
</a>{" "}
for a complete tutorial on scraping and visualizing MinIO
metrics with Prometheus.
</Fragment>
}
/>
{!usage?.prometheusNotReady && (
<Grid
container
justifyContent={"center"}
alignContent={"center"}
alignItems={"center"}
>
<Grid item xs={8}>
<HelpBox
iconComponent={<PrometheusIcon />}
title={"Monitoring"}
help={
<Fragment>
The MinIO Dashboard is displaying basic metrics only due to
missing the{" "}
<a
href="https://docs.min.io/minio/baremetal/console/minio-console.html?ref=con#configuration"
target="_blank"
rel="noreferrer"
>
necessary settings
</a>{" "}
for displaying extended metrics.
<br />
<br />
See{" "}
<a
href="https://docs.min.io/minio/baremetal/monitoring/metrics-alerts/collect-minio-metrics-using-prometheus.html?ref=con#minio-metrics-collect-using-prometheus"
target="_blank"
rel="noreferrer"
>
Collect MinIO Metrics Using Prometheus
</a>{" "}
for a complete tutorial on scraping and visualizing MinIO
metrics with Prometheus.
</Fragment>
}
/>
</Grid>
</Grid>
</Grid>
)}
</Fragment>
);
};

View File

@@ -154,7 +154,12 @@ const LinearGraphWidget = ({
if (key === "name") {
continue;
}
const val = parseInt(dp[key]);
let val = parseInt(dp[key]);
if (isNaN(val)) {
val = 0;
}
if (maxVal < val) {
maxVal = val;
}

View File

@@ -111,6 +111,18 @@ const SingleRepWidget = ({
}, [loading, panelItem, timeEnd, timeStart, displayErrorMessage, apiPrefix]);
const gradientID = `colorGradient-${title.split(" ").join("-")}`;
let repNumber = "";
if (result) {
const resultRep = parseInt(result.innerLabel || "0");
if (!isNaN(resultRep)) {
repNumber = representationNumber(resultRep);
} else {
repNumber = "0";
}
}
return (
<div className={classes.singleValueContainer}>
<div className={classes.titleContainer}>{title}</div>
@@ -150,7 +162,7 @@ const SingleRepWidget = ({
fill={"#07193E"}
>
{result
? representationNumber(parseInt(result.innerLabel || "0"))
? repNumber
: ""}
</text>
</AreaChart>

View File

@@ -20,6 +20,7 @@ export interface Usage {
usage: number;
buckets: number;
objects: number;
prometheusNotReady?: boolean;
widgets?: any;
servers: ServerInfo[];
}

View File

@@ -14,38 +14,19 @@
// 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, { useEffect, useState } from "react";
import React from "react";
import { connect } from "react-redux";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
} from "@mui/material";
import api from "../../../common/api";
import { DialogContentText } from "@mui/material";
import { setErrorSnackMessage } from "../../../actions";
import { ErrorResponseHandler } from "../../../common/types";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import IconButton from "@mui/material/IconButton";
import CloseIcon from "@mui/icons-material/Close";
import { deleteDialogStyles } from "../Common/FormComponents/common/styleLibrary";
const styles = (theme: Theme) =>
createStyles({
...deleteDialogStyles,
});
import ConfirmDialog from "../Common/ModalWrapper/ConfirmDialog";
import useApi from "../Common/Hooks/useApi";
interface IDeleteGroup {
selectedGroup: string;
deleteOpen: boolean;
closeDeleteModalAndRefresh: any;
setErrorSnackMessage: typeof setErrorSnackMessage;
classes: any;
}
const DeleteGroup = ({
@@ -53,94 +34,36 @@ const DeleteGroup = ({
deleteOpen,
closeDeleteModalAndRefresh,
setErrorSnackMessage,
classes,
}: IDeleteGroup) => {
const [isDeleting, setDeleteLoading] = useState<boolean>(false);
const onDelSuccess = () => closeDeleteModalAndRefresh(true);
const onDelError = (err: ErrorResponseHandler) => setErrorSnackMessage(err);
const onClose = () => closeDeleteModalAndRefresh(false);
useEffect(() => {
if (isDeleting) {
const removeRecord = () => {
if (!selectedGroup) {
return;
}
const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError);
api
.invoke("DELETE", `/api/v1/group?name=${encodeURI(selectedGroup)}`)
.then(() => {
setDeleteLoading(false);
closeDeleteModalAndRefresh(true);
})
.catch((err: ErrorResponseHandler) => {
setDeleteLoading(false);
setErrorSnackMessage(err);
});
};
removeRecord();
}
}, [
isDeleting,
selectedGroup,
closeDeleteModalAndRefresh,
setErrorSnackMessage,
]);
const closeNoAction = () => {
closeDeleteModalAndRefresh(false);
if (!selectedGroup) {
return null;
}
const onDeleteGroup = () => {
invokeDeleteApi("DELETE", `/api/v1/group?name=${encodeURI(selectedGroup)}`);
};
return (
<Dialog
open={deleteOpen}
onClose={closeNoAction}
classes={classes}
className={classes.root}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title" className={classes.title}>
<div className={classes.titleText}>Delete Group</div>
<div className={classes.closeContainer}>
<IconButton
aria-label="close"
className={classes.closeButton}
onClick={closeNoAction}
disableRipple
size="small"
>
<CloseIcon />
</IconButton>
</div>
</DialogTitle>
<DialogContent>
{isDeleting && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete group <br />
<ConfirmDialog
title={`Delete Group`}
confirmText={"Delete"}
isOpen={deleteOpen}
isLoading={deleteLoading}
onConfirm={onDeleteGroup}
onClose={onClose}
confirmationContent={
<DialogContentText>
Are you sure you want to delete group
<br />
<b>{selectedGroup}</b>?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
type="button"
variant="outlined"
onClick={closeNoAction}
color="primary"
disabled={isDeleting}
>
Cancel
</Button>
<Button
type="button"
variant="outlined"
onClick={() => {
setDeleteLoading(true);
}}
color="secondary"
autoFocus
>
Delete
</Button>
</DialogActions>
</Dialog>
}
/>
);
};
@@ -150,4 +73,4 @@ const mapDispatchToProps = {
const connector = connect(null, mapDispatchToProps);
export default withStyles(styles)(connector(DeleteGroup));
export default connector(DeleteGroup);

View File

@@ -42,14 +42,12 @@ import api from "../../../common/api";
import MenuIcon from "@mui/icons-material/Menu";
import LogoutIcon from "../../../icons/LogoutIcon";
import { resetSession } from "../actions";
const drawerWidth = 245;
const BucketsIcon = React.lazy(() => import("../../../icons/BucketsIcon"));
const DashboardIcon = React.lazy(() => import("../../../icons/DashboardIcon"));
const DiagnosticsIcon = React.lazy(
() => import("../../../icons/DiagnosticsIcon")
);
const GroupsIcon = React.lazy(() => import("../../../icons/GroupsIcon"));
const IAMPoliciesIcon = React.lazy(
() => import("../../../icons/IAMPoliciesIcon")
@@ -61,7 +59,6 @@ const UsersIcon = React.lazy(() => import("../../../icons/UsersIcon"));
const VersionIcon = React.lazy(() => import("../../../icons/VersionIcon"));
const LicenseIcon = React.lazy(() => import("../../../icons/LicenseIcon"));
const HealIcon = React.lazy(() => import("../../../icons/HealIcon"));
const AccountIcon = React.lazy(() => import("../../../icons/AccountIcon"));
const DocumentationIcon = React.lazy(
() => import("../../../icons/DocumentationIcon")
@@ -290,6 +287,7 @@ interface IMenuProps {
distributedSetup: boolean;
sidebarOpen: boolean;
setMenuOpen: typeof setMenuOpen;
resetSession: typeof resetSession;
}
const Menu = ({
@@ -300,13 +298,14 @@ const Menu = ({
distributedSetup,
sidebarOpen,
setMenuOpen,
resetSession,
}: IMenuProps) => {
const logout = () => {
const deleteSession = () => {
clearSession();
userLoggedIn(false);
localStorage.setItem("userLoggedIn", "");
resetSession();
history.push("/login");
};
api
@@ -402,23 +401,6 @@ const Menu = ({
name: "Tools",
icon: ToolsIcon,
},
{
group: "Tools",
type: "item",
component: NavLink,
to: "/heal",
name: "Heal",
icon: HealIcon,
fsHidden: distributedSetup,
},
{
group: "Tools",
type: "item",
component: NavLink,
to: "/health-info",
name: "Diagnostic",
icon: DiagnosticsIcon,
},
{
group: "Operator",
type: "item",
@@ -438,6 +420,9 @@ const Menu = ({
];
const allowedPages = pages.reduce((result: any, item: any) => {
if (item.startsWith("/tools")) {
result["/tools"] = true;
}
result[item] = true;
return result;
}, {});
@@ -643,6 +628,10 @@ const mapState = (state: AppState) => ({
distributedSetup: state.system.distributedSetup,
});
const connector = connect(mapState, { userLoggedIn, setMenuOpen });
const connector = connect(mapState, {
userLoggedIn,
setMenuOpen,
resetSession,
});
export default connector(withStyles(styles)(Menu));

View File

@@ -38,6 +38,7 @@ import {
IElementValue,
} from "../../Configurations/types";
import { ErrorResponseHandler } from "../../../../common/types";
import ResetConfigurationModal from "./ResetConfigurationModal";
const styles = (theme: Theme) =>
createStyles({
@@ -84,24 +85,31 @@ const EditConfiguration = ({
const [saving, setSaving] = useState<boolean>(false);
const [loadingConfig, setLoadingConfig] = useState<boolean>(true);
const [configValues, setConfigValues] = useState<IElementValue[]>([]);
const [resetConfigurationOpen, setResetConfigurationOpen] =
useState<boolean>(false);
//Effects
useEffect(() => {
const configId = get(selectedConfiguration, "configuration_id", false);
if (loadingConfig) {
const configId = get(selectedConfiguration, "configuration_id", false);
if (configId) {
api
.invoke("GET", `/api/v1/configs/${configId}`)
.then((res) => {
const keyVals = get(res, "key_values", []);
setConfigValues(keyVals);
})
.catch((err: ErrorResponseHandler) => {
setLoadingConfig(false);
setErrorSnackMessage(err);
});
if (configId) {
api
.invoke("GET", `/api/v1/configs/${configId}`)
.then((res) => {
const keyVals = get(res, "key_values", []);
setConfigValues(keyVals);
setLoadingConfig(false);
})
.catch((err: ErrorResponseHandler) => {
setLoadingConfig(false);
setErrorSnackMessage(err);
});
return;
}
setLoadingConfig(false);
}
setLoadingConfig(false);
}, [selectedConfiguration, setErrorSnackMessage]);
}, [loadingConfig, selectedConfiguration, setErrorSnackMessage]);
useEffect(() => {
if (saving) {
@@ -147,8 +155,24 @@ const EditConfiguration = ({
[setValueObj]
);
const continueReset = (restart: boolean) => {
setResetConfigurationOpen(false);
serverNeedsRestart(restart);
if (restart) {
setLoadingConfig(true);
}
};
return (
<Fragment>
{resetConfigurationOpen && (
<ResetConfigurationModal
configurationName={selectedConfiguration.configuration_id}
closeResetModalAndRefresh={continueReset}
resetOpen={resetConfigurationOpen}
/>
)}
<form noValidate onSubmit={submitForm} className={className}>
<Grid item xs={12} className={classes.settingsFormContainer}>
{loadingConfig && (
@@ -165,6 +189,17 @@ const EditConfiguration = ({
/>
</Grid>
<Grid item xs={12} className={classes.settingsButtonContainer}>
<Button
type="button"
variant="outlined"
color="secondary"
onClick={() => {
setResetConfigurationOpen(true);
}}
>
Restore Defaults
</Button>
&nbsp; &nbsp;
<Button
type="submit"
variant="contained"

View File

@@ -0,0 +1,163 @@
// 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, { useEffect, useState } from "react";
import { connect } from "react-redux";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
} from "@mui/material";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import IconButton from "@mui/material/IconButton";
import CloseIcon from "@mui/icons-material/Close";
import {
deleteDialogStyles,
modalBasic,
} from "../../Common/FormComponents/common/styleLibrary";
import { setErrorSnackMessage } from "../../../../actions";
import { ErrorResponseHandler } from "../../../../common/types";
import api from "../../../../common/api";
const styles = (theme: Theme) =>
createStyles({
wrapText: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word",
},
...modalBasic,
...deleteDialogStyles,
});
interface IResetConfiguration {
classes: any;
configurationName: string;
closeResetModalAndRefresh: (reloadConfiguration: boolean) => void;
setErrorSnackMessage: typeof setErrorSnackMessage;
resetOpen: boolean;
}
const ResetConfigurationModal = ({
classes,
configurationName,
closeResetModalAndRefresh,
setErrorSnackMessage,
resetOpen,
}: IResetConfiguration) => {
const [resetLoading, setResetLoading] = useState<boolean>(false);
useEffect(() => {
if (resetLoading) {
api
.invoke("GET", `/api/v1/configs/${configurationName}/reset`)
.then((res) => {
setResetLoading(false);
closeResetModalAndRefresh(true);
})
.catch((err: ErrorResponseHandler) => {
setResetLoading(false);
setErrorSnackMessage(err);
});
}
}, [
closeResetModalAndRefresh,
configurationName,
resetLoading,
setErrorSnackMessage,
]);
const resetConfiguration = () => {
setResetLoading(true);
};
return (
<Dialog
open={resetOpen}
classes={classes}
onClose={() => {
closeResetModalAndRefresh(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title" className={classes.title}>
<div className={classes.titleText}>Restore Defaults</div>
<div className={classes.closeContainer}>
<IconButton
aria-label="close"
className={classes.closeButton}
onClick={() => {
closeResetModalAndRefresh(false);
}}
disableRipple
size="small"
>
<CloseIcon />
</IconButton>
</div>
</DialogTitle>
<DialogContent>
{resetLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
Are you sure you want to restore these configurations to default
values?
<br />
<b className={classes.wrapText}>
Please note that this may cause your system to not be accessible
</b>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
type="button"
variant="outlined"
onClick={() => {
closeResetModalAndRefresh(false);
}}
color="primary"
disabled={resetLoading}
>
Cancel
</Button>
<Button
onClick={resetConfiguration}
variant="contained"
color="primary"
autoFocus
disabled={resetLoading}
>
Yes, Reset Configuration
</Button>
</DialogActions>
</Dialog>
);
};
const mapDispatchToProps = {
setErrorSnackMessage,
};
const connector = connect(null, mapDispatchToProps);
export default withStyles(styles)(connector(ResetConfigurationModal));

View File

@@ -14,123 +14,56 @@
// 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 React from "react";
import { connect } from "react-redux";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
} from "@mui/material";
import api from "../../../common/api";
import { PolicyList } from "./types";
import { DialogContentText } from "@mui/material";
import { setErrorSnackMessage } from "../../../actions";
import { ErrorResponseHandler } from "../../../common/types";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import { deleteDialogStyles } from "../Common/FormComponents/common/styleLibrary";
import withStyles from "@mui/styles/withStyles";
import IconButton from "@mui/material/IconButton";
import CloseIcon from "@mui/icons-material/Close";
import useApi from "../Common/Hooks/useApi";
import ConfirmDialog from "../Common/ModalWrapper/ConfirmDialog";
interface IDeletePolicyProps {
closeDeleteModalAndRefresh: (refresh: boolean) => void;
deleteOpen: boolean;
selectedPolicy: string;
setErrorSnackMessage: typeof setErrorSnackMessage;
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
...deleteDialogStyles,
});
const DeletePolicy = ({
classes,
closeDeleteModalAndRefresh,
deleteOpen,
selectedPolicy,
setErrorSnackMessage,
}: IDeletePolicyProps) => {
const [deleteLoading, setDeleteLoading] = useState<boolean>(false);
const removeRecord = () => {
if (deleteLoading) {
return;
}
setDeleteLoading(true);
api
.invoke("DELETE", `/api/v1/policy?name=${selectedPolicy}`)
.then((res: PolicyList) => {
setDeleteLoading(false);
const onDelSuccess = () => closeDeleteModalAndRefresh(true);
const onDelError = (err: ErrorResponseHandler) => setErrorSnackMessage(err);
const onClose = () => closeDeleteModalAndRefresh(false);
closeDeleteModalAndRefresh(true);
})
.catch((err: ErrorResponseHandler) => {
setDeleteLoading(false);
setErrorSnackMessage(err);
});
const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError);
if (!selectedPolicy) {
return null;
}
const onConfirmDelete = () => {
invokeDeleteApi("DELETE", `/api/v1/policy?name=${selectedPolicy}`);
};
return (
<Dialog
classes={classes}
className={classes.root}
open={deleteOpen}
onClose={() => {
closeDeleteModalAndRefresh(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title" className={classes.title}>
<div className={classes.titleText}>Delete Policy</div>
<div className={classes.closeContainer}>
<IconButton
aria-label="close"
className={classes.closeButton}
onClick={() => {
closeDeleteModalAndRefresh(false);
}}
disableRipple
size="small"
>
<CloseIcon />
</IconButton>
</div>
</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
<ConfirmDialog
title={`Delete Policy`}
confirmText={"Delete"}
isOpen={deleteOpen}
isLoading={deleteLoading}
onConfirm={onConfirmDelete}
onClose={onClose}
confirmationContent={
<DialogContentText>
Are you sure you want to delete policy <br />
<b>{selectedPolicy}</b>?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
variant="outlined"
onClick={() => {
closeDeleteModalAndRefresh(false);
}}
color="primary"
disabled={deleteLoading}
>
Cancel
</Button>
<Button
variant="outlined"
onClick={() => {
removeRecord();
}}
color="secondary"
autoFocus
>
Delete
</Button>
</DialogActions>
</Dialog>
}
/>
);
};
@@ -139,4 +72,4 @@ const mapDispatchToProps = {
};
const connector = connect(null, mapDispatchToProps);
export default withStyles(styles)(connector(DeletePolicy));
export default connector(DeletePolicy);

View File

@@ -14,23 +14,16 @@
// 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, { useEffect, useState } from "react";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
} from "@mui/material";
import api from "../../../../common/api";
import React, { useState } from "react";
import { DialogContentText } from "@mui/material";
import { ITenant } from "./types";
import { connect } from "react-redux";
import { setErrorSnackMessage } from "../../../../actions";
import { ErrorResponseHandler } from "../../../../common/types";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import Grid from "@mui/material/Grid";
import useApi from "../../Common/Hooks/useApi";
import ConfirmDialog from "../../Common/ModalWrapper/ConfirmDialog";
interface IDeleteTenant {
deleteOpen: boolean;
@@ -45,29 +38,15 @@ const DeleteTenant = ({
closeDeleteModalAndRefresh,
setErrorSnackMessage,
}: IDeleteTenant) => {
const [deleteLoading, setDeleteLoading] = useState(false);
const [retypeTenant, setRetypeTenant] = useState("");
useEffect(() => {
if (deleteLoading) {
api
.invoke(
"DELETE",
`/api/v1/namespaces/${selectedTenant.namespace}/tenants/${selectedTenant.name}`
)
.then(() => {
setDeleteLoading(false);
closeDeleteModalAndRefresh(true);
})
.catch((err: ErrorResponseHandler) => {
setDeleteLoading(false);
setErrorSnackMessage(err);
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [deleteLoading]);
const onDelSuccess = () => closeDeleteModalAndRefresh(true);
const onDelError = (err: ErrorResponseHandler) => setErrorSnackMessage(err);
const onClose = () => closeDeleteModalAndRefresh(false);
const removeRecord = () => {
const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError);
const onConfirmDelete = () => {
if (retypeTenant !== selectedTenant.name) {
setErrorSnackMessage({
errorMessage: "Tenant name is incorrect",
@@ -75,22 +54,25 @@ const DeleteTenant = ({
});
return;
}
setDeleteLoading(true);
invokeDeleteApi(
"DELETE",
`/api/v1/namespaces/${selectedTenant.namespace}/tenants/${selectedTenant.name}`
);
};
return (
<Dialog
open={deleteOpen}
onClose={() => {
closeDeleteModalAndRefresh(false);
<ConfirmDialog
title={`Delete Tenant`}
confirmText={"Delete"}
isOpen={deleteOpen}
isLoading={deleteLoading}
onConfirm={onConfirmDelete}
onClose={onClose}
confirmButtonProps={{
disabled: retypeTenant !== selectedTenant.name || deleteLoading,
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete Tenant</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
confirmationContent={
<DialogContentText>
To continue please type <b>{selectedTenant.name}</b> in the box.
<Grid item xs={12}>
<InputBoxWrapper
@@ -104,27 +86,8 @@ const DeleteTenant = ({
/>
</Grid>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
closeDeleteModalAndRefresh(false);
}}
color="primary"
disabled={deleteLoading}
>
Cancel
</Button>
<Button
onClick={removeRecord}
color="secondary"
autoFocus
disabled={retypeTenant !== selectedTenant.name}
>
Delete
</Button>
</DialogActions>
</Dialog>
}
/>
);
};

View File

@@ -1,83 +0,0 @@
import React, { useState } from "react";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import {
containerForHeader,
tenantDetailsStyles,
} from "../../Common/FormComponents/common/styleLibrary";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
} from "@mui/material";
interface IConfirmationDialog {
classes: any;
open: boolean;
cancelLabel: string;
okLabel: string;
onClose: any;
cancelOnClick: any;
okOnClick: any;
title: string;
description: string;
}
const styles = (theme: Theme) =>
createStyles({
...tenantDetailsStyles,
...containerForHeader(theme.spacing(4)),
});
const ConfirmationDialog = ({
classes,
open,
cancelLabel,
okLabel,
onClose,
cancelOnClick,
okOnClick,
title,
description,
}: IConfirmationDialog) => {
const [isSending, setIsSending] = useState<boolean>(false);
const onClick = () => {
setIsSending(true);
if (okOnClick !== null) {
okOnClick();
}
setIsSending(false);
};
if (!open) return null;
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">{title}</DialogTitle>
<DialogContent>
{isSending && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
{description}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={cancelOnClick} color="primary" disabled={isSending}>
{cancelLabel || "Cancel"}
</Button>
<Button onClick={onClick} color="secondary" autoFocus>
{okLabel || "Ok"}
</Button>
</DialogActions>
</Dialog>
);
};
export default withStyles(styles)(ConfirmationDialog);

View File

@@ -14,23 +14,16 @@
// 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, { useEffect, useState } from "react";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
} from "@mui/material";
import api from "../../../../common/api";
import React, { useState } from "react";
import { DialogContentText } from "@mui/material";
import { IPodListElement } from "../ListTenants/types";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import Grid from "@mui/material/Grid";
import { connect } from "react-redux";
import { setErrorSnackMessage } from "../../../../actions";
import { ErrorResponseHandler } from "../../../../common/types";
import useApi from "../../Common/Hooks/useApi";
import ConfirmDialog from "../../Common/ModalWrapper/ConfirmDialog";
interface IDeletePod {
deleteOpen: boolean;
@@ -45,29 +38,15 @@ const DeletePod = ({
closeDeleteModalAndRefresh,
setErrorSnackMessage,
}: IDeletePod) => {
const [deleteLoading, setDeleteLoading] = useState(false);
const [retypePod, setRetypePod] = useState("");
useEffect(() => {
if (deleteLoading) {
api
.invoke(
"DELETE",
`/api/v1/namespaces/${selectedPod.namespace}/tenants/${selectedPod.tenant}/pods/${selectedPod.name}`
)
.then(() => {
setDeleteLoading(false);
closeDeleteModalAndRefresh(true);
})
.catch((err: ErrorResponseHandler) => {
setDeleteLoading(false);
setErrorSnackMessage(err);
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [deleteLoading]);
const onDelSuccess = () => closeDeleteModalAndRefresh(true);
const onDelError = (err: ErrorResponseHandler) => setErrorSnackMessage(err);
const onClose = () => closeDeleteModalAndRefresh(false);
const removeRecord = () => {
const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError);
const onConfirmDelete = () => {
if (retypePod !== selectedPod.name) {
setErrorSnackMessage({
errorMessage: "Tenant name is incorrect",
@@ -75,22 +54,25 @@ const DeletePod = ({
});
return;
}
setDeleteLoading(true);
invokeDeleteApi(
"DELETE",
`/api/v1/namespaces/${selectedPod.namespace}/tenants/${selectedPod.tenant}/pods/${selectedPod.name}`
);
};
return (
<Dialog
open={deleteOpen}
onClose={() => {
closeDeleteModalAndRefresh(false);
<ConfirmDialog
title={`Delete Pod`}
confirmText={"Delete"}
isOpen={deleteOpen}
isLoading={deleteLoading}
onConfirm={onConfirmDelete}
onClose={onClose}
confirmButtonProps={{
disabled: retypePod !== selectedPod.name || deleteLoading,
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete Pod</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
confirmationContent={
<DialogContentText>
To continue please type <b>{selectedPod.name}</b> in the box.
<Grid item xs={12}>
<InputBoxWrapper
@@ -104,27 +86,8 @@ const DeletePod = ({
/>
</Grid>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
closeDeleteModalAndRefresh(false);
}}
color="primary"
disabled={deleteLoading}
>
Cancel
</Button>
<Button
onClick={removeRecord}
color="secondary"
autoFocus
disabled={retypePod !== selectedPod.name}
>
Delete
</Button>
</DialogActions>
</Dialog>
}
/>
);
};

View File

@@ -29,7 +29,12 @@ import Chip from "@mui/material/Chip";
import React, { Fragment, useCallback, useEffect, useState } from "react";
import Moment from "react-moment";
import FormSwitchWrapper from "../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import { Button, CircularProgress, Typography } from "@mui/material";
import {
Button,
CircularProgress,
DialogContentText,
Typography,
} from "@mui/material";
import { KeyPair } from "../ListTenants/utils";
import FileSelector from "../../Common/FormComponents/FileSelector/FileSelector";
import api from "../../../../common/api";
@@ -38,7 +43,7 @@ import { connect } from "react-redux";
import { AppState } from "../../../../store";
import { ErrorResponseHandler } from "../../../../common/types";
import { setTenantDetailsLoad } from "../actions";
import ConfirmationDialog from "./ConfirmationDialog";
import ConfirmDialog from "../../Common/ModalWrapper/ConfirmDialog";
interface ITenantSecurity {
classes: any;
@@ -311,15 +316,19 @@ const TenantSecurity = ({
};
return (
<React.Fragment>
<ConfirmationDialog
open={dialogOpen}
title="Save and Restart"
description="Are you sure you want to save the changes and restart the service?"
<ConfirmDialog
title={"Save and Restart"}
confirmText={"Restart"}
cancelText="Cancel"
isLoading={isSending}
onClose={() => setDialogOpen(false)}
cancelOnClick={() => setDialogOpen(false)}
okOnClick={updateTenantSecurity}
cancelLabel="Cancel"
okLabel={"Restart"}
isOpen={dialogOpen}
onConfirm={updateTenantSecurity}
confirmationContent={
<DialogContentText>
Are you sure you want to save the changes and restart the service?
</DialogContentText>
}
/>
{loadingTenant ? (
<Paper className={classes.paperContainer}>

View File

@@ -20,7 +20,6 @@ import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import Grid from "@mui/material/Grid";
import { configurationElements } from "../utils";
import {
actionsTray,
containerForHeader,
@@ -29,6 +28,21 @@ import {
import PageHeader from "../../Common/PageHeader/PageHeader";
import SettingsCard from "../../Common/SettingsCard/SettingsCard";
import PageLayout from "../../Common/Layout/PageLayout";
import { IElement } from "../types";
import {
DiagnosticsIcon,
HealIcon,
LogsIcon,
SearchIcon,
TraceIcon,
WatchIcon,
} from "../../../../icons";
import { hasPermission } from "../../../../common/SecureComponent/SecureComponent";
import {
CONSOLE_UI_RESOURCE,
IAM_SCOPES,
} from "../../../../common/SecureComponent/permissions";
import SpeedtestIcon from "../../../../icons/SpeedtestIcon";
interface IConfigurationOptions {
classes: any;
@@ -76,6 +90,59 @@ const styles = (theme: Theme) =>
});
const ToolsList = ({ classes }: IConfigurationOptions) => {
const configurationElements: IElement[] = [
{
icon: <LogsIcon />,
configuration_id: "logs",
configuration_label: "Logs",
disabled: !hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.ADMIN_CONSOLE_LOG_ACTION,
]),
},
{
icon: <SearchIcon />,
configuration_id: "audit-logs",
configuration_label: "Audit Logs",
},
{
icon: <WatchIcon />,
configuration_id: "watch",
configuration_label: "Watch",
},
{
icon: <TraceIcon />,
configuration_id: "trace",
configuration_label: "Trace",
disabled: !hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.ADMIN_TRACE_ACTION,
]),
},
{
icon: <HealIcon />,
configuration_id: "heal",
configuration_label: "Heal",
disabled: !hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.ADMIN_HEAL_ACTION,
]),
},
{
icon: <DiagnosticsIcon />,
configuration_id: "diagnostics",
configuration_label: "Diagnostics",
disabled: !hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.ADMIN_HEALTH_ACTION,
]),
},
{
icon: <SpeedtestIcon />,
configuration_id: "speedtest",
configuration_label: "Speedtest",
disabled: !hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.ADMIN_HEAL_ACTION,
]),
},
];
return (
<Fragment>
<PageHeader label={"Tools"} />

View File

@@ -1,75 +0,0 @@
// 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 from "react";
import { IElement } from "./types";
import {
DiagnosticsIcon,
HealIcon,
LogsIcon,
SearchIcon,
TraceIcon,
WatchIcon,
} from "../../../icons";
import SpeedtestIcon from "../../../icons/SpeedtestIcon";
import {
CONSOLE_UI_RESOURCE,
IAM_SCOPES,
} from "../../../common/SecureComponent/permissions";
import { hasPermission } from "../../../common/SecureComponent/SecureComponent";
export const configurationElements: IElement[] = [
{
icon: <LogsIcon />,
configuration_id: "logs",
configuration_label: "Logs",
},
{
icon: <SearchIcon />,
configuration_id: "audit-logs",
configuration_label: "Audit Logs",
},
{
icon: <WatchIcon />,
configuration_id: "watch",
configuration_label: "Watch",
},
{
icon: <TraceIcon />,
configuration_id: "trace",
configuration_label: "trace",
},
{
icon: <HealIcon />,
configuration_id: "heal",
configuration_label: "heal",
disabled: !hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.ADMIN_HEAL_ACTION,
]),
},
{
icon: <DiagnosticsIcon />,
configuration_id: "diagnostics",
configuration_label: "Diagnostics",
},
{
icon: <SpeedtestIcon />,
configuration_id: "speedtest",
configuration_label: "Speedtest",
disabled: !hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.ADMIN_HEAL_ACTION,
]),
},
];

View File

@@ -14,141 +14,63 @@
// 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 React from "react";
import { connect } from "react-redux";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
} from "@mui/material";
import api from "../../../common/api";
import { User, UsersList } from "./types";
import { DialogContentText } from "@mui/material";
import { User } from "./types";
import { setErrorSnackMessage } from "../../../actions";
import useApi from "../Common/Hooks/useApi";
import ConfirmDialog from "../Common/ModalWrapper/ConfirmDialog";
import { ErrorResponseHandler } from "../../../common/types";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import { deleteDialogStyles } from "../Common/FormComponents/common/styleLibrary";
import IconButton from "@mui/material/IconButton";
import CloseIcon from "@mui/icons-material/Close";
import withStyles from "@mui/styles/withStyles";
const styles = (theme: Theme) =>
createStyles({
...deleteDialogStyles,
});
interface IDeleteUserProps {
closeDeleteModalAndRefresh: (refresh: boolean) => void;
deleteOpen: boolean;
selectedUser: User | null;
setErrorSnackMessage: typeof setErrorSnackMessage;
classes: any;
}
const DeleteUser = ({
classes,
closeDeleteModalAndRefresh,
deleteOpen,
selectedUser,
setErrorSnackMessage,
}: IDeleteUserProps) => {
const [deleteLoading, setDeleteLoading] = useState<boolean>(false);
const onDelSuccess = () => closeDeleteModalAndRefresh(true);
const onDelError = (err: ErrorResponseHandler) => setErrorSnackMessage(err);
const onClose = () => closeDeleteModalAndRefresh(false);
const removeRecord = () => {
if (deleteLoading) {
return;
}
if (selectedUser === null) {
return;
}
setDeleteLoading(true);
api
.invoke(
"DELETE",
`/api/v1/user?name=${encodeURI(selectedUser.accessKey)}`,
{
id: selectedUser.id,
}
)
.then((res: UsersList) => {
setDeleteLoading(false);
closeDeleteModalAndRefresh(true);
})
.catch((err: ErrorResponseHandler) => {
setDeleteLoading(false);
setErrorSnackMessage(err);
});
};
const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError);
if (selectedUser === null) {
return <div />;
if (!selectedUser) {
return null;
}
return (
<Dialog
open={deleteOpen}
onClose={() => {
closeDeleteModalAndRefresh(false);
}}
classes={classes}
className={classes.root}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title" className={classes.title}>
<div className={classes.titleText}>Delete User</div>
<div className={classes.closeContainer}>
<IconButton
aria-label="close"
className={classes.closeButton}
onClick={() => {
closeDeleteModalAndRefresh(true);
}}
disableRipple
size="small"
>
<CloseIcon />
</IconButton>
</div>
</DialogTitle>
const onConfirmDelete = () => {
invokeDeleteApi(
"DELETE",
`/api/v1/user?name=${encodeURI(selectedUser.accessKey)}`,
{
id: selectedUser.id,
}
);
};
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
return (
<ConfirmDialog
title={`Delete User`}
confirmText={"Delete"}
isOpen={deleteOpen}
isLoading={deleteLoading}
onConfirm={onConfirmDelete}
onClose={onClose}
confirmationContent={
<DialogContentText>
Are you sure you want to delete user <br />
<b>{selectedUser.accessKey}</b>?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
type="button"
variant="outlined"
onClick={() => {
closeDeleteModalAndRefresh(false);
}}
color="primary"
disabled={deleteLoading}
>
Cancel
</Button>
<Button
onClick={() => {
removeRecord();
}}
type="button"
variant="outlined"
color="secondary"
autoFocus
>
Delete
</Button>
</DialogActions>
</Dialog>
}
/>
);
};
@@ -158,4 +80,4 @@ const mapDispatchToProps = {
const connector = connect(null, mapDispatchToProps);
export default withStyles(styles)(connector(DeleteUser));
export default connector(DeleteUser);

View File

@@ -14,22 +14,14 @@
// 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 React from "react";
import { connect } from "react-redux";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
} from "@mui/material";
import { DialogContentText } from "@mui/material";
import { setErrorSnackMessage } from "../../../actions";
import { UsersList } from "./types";
import { ErrorResponseHandler } from "../../../common/types";
import history from "../../../history";
import api from "../../../common/api";
import useApi from "../Common/Hooks/useApi";
import ConfirmDialog from "../Common/ModalWrapper/ConfirmDialog";
interface IDeleteUserProps {
closeDeleteModalAndRefresh: (refresh: boolean) => void;
@@ -44,73 +36,39 @@ const DeleteUserString = ({
userName,
setErrorSnackMessage,
}: IDeleteUserProps) => {
const [deleteLoading, setDeleteLoading] = useState<boolean>(false);
const removeRecord = () => {
if (deleteLoading) {
return;
}
if (userName === null) {
return;
}
setDeleteLoading(true);
api
.invoke("DELETE", `/api/v1/user?name=${encodeURI(userName)}`, {
id: userName,
})
.then((res: UsersList) => {
setDeleteLoading(false);
closeDeleteModalAndRefresh(true);
})
.catch((err: ErrorResponseHandler) => {
setDeleteLoading(false);
setErrorSnackMessage(err);
});
const onDelSuccess = () => {
history.push(`/users/`);
};
const onDelError = (err: ErrorResponseHandler) => setErrorSnackMessage(err);
const onClose = () => closeDeleteModalAndRefresh(false);
if (userName === null) {
return <div />;
const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError);
if (!userName) {
return null;
}
const onConfirmDelete = () => {
invokeDeleteApi("DELETE", `/api/v1/user?name=${encodeURI(userName)}`, {
id: userName,
});
};
return (
<Dialog
open={deleteOpen}
onClose={() => {
closeDeleteModalAndRefresh(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete User</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete user <b>{userName}</b>?
<ConfirmDialog
title={`Delete User`}
confirmText={"Delete"}
isOpen={deleteOpen}
isLoading={deleteLoading}
onConfirm={onConfirmDelete}
onClose={onClose}
confirmationContent={
<DialogContentText>
Are you sure you want to delete user <br />
<b>{userName}</b>?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
closeDeleteModalAndRefresh(false);
}}
color="primary"
disabled={deleteLoading}
>
Cancel
</Button>
<Button
onClick={() => {
removeRecord();
closeDeleteModalAndRefresh(true);
history.push(`/users/`);
}}
color="secondary"
autoFocus
>
Delete
</Button>
</DialogActions>
</Dialog>
}
/>
);
};

View File

@@ -17,12 +17,18 @@
import { ISessionResponse } from "./types";
export const SESSION_RESPONSE = "SESSION_RESPONSE";
export const RESET_SESSION = "RESET_SESSION";
interface SessionAction {
type: typeof SESSION_RESPONSE;
message: ISessionResponse;
}
export type SessionActionTypes = SessionAction;
interface ResetSessionAction {
type: typeof RESET_SESSION;
}
export type SessionActionTypes = SessionAction | ResetSessionAction;
export function saveSessionResponse(message: ISessionResponse) {
return {
@@ -30,3 +36,9 @@ export function saveSessionResponse(message: ISessionResponse) {
message: message,
};
}
export function resetSession() {
return {
type: RESET_SESSION,
};
}

View File

@@ -15,7 +15,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { ISessionResponse } from "./types";
import { SessionActionTypes, SESSION_RESPONSE } from "./actions";
import { RESET_SESSION, SESSION_RESPONSE, SessionActionTypes } from "./actions";
export interface ConsoleState {
session: ISessionResponse;
@@ -42,6 +42,11 @@ export function consoleReducer(
...state,
session: action.message,
};
case RESET_SESSION:
return {
...state,
session: initialState.session,
};
default:
return state;
}

View File

@@ -55,6 +55,15 @@ func registerConfigHandlers(api *operations.ConsoleAPI) {
}
return admin_api.NewSetConfigOK().WithPayload(resp)
})
// Reset Configuration
api.AdminAPIResetConfigHandler = admin_api.ResetConfigHandlerFunc(func(params admin_api.ResetConfigParams, session *models.Principal) middleware.Responder {
resp, err := resetConfigResponse(session, params.Name)
if err != nil {
return admin_api.NewResetConfigDefault(int(err.Code)).WithPayload(err)
}
return admin_api.NewResetConfigOK().WithPayload(resp)
})
}
// listConfig gets all configurations' names and their descriptions
@@ -198,3 +207,29 @@ func setConfigResponse(session *models.Principal, name string, configRequest *mo
}
return &models.SetConfigResponse{Restart: needsRestart}, nil
}
func resetConfig(ctx context.Context, client MinioAdmin, configName *string) (err error) {
err = client.delConfigKV(ctx, *configName)
return err
}
// resetConfigResponse implements resetConfig() to be used by handler
func resetConfigResponse(session *models.Principal, configName string) (*models.SetConfigResponse, *models.Error) {
mAdmin, err := NewMinioAdminClient(session)
if err != nil {
return nil, prepareError(err)
}
// create a MinIO Admin Client interface implementation
// defining the client to be used
adminClient := AdminClient{Client: mAdmin}
ctx := context.Background()
err = resetConfig(ctx, adminClient, &configName)
if err != nil {
return nil, prepareError(err)
}
return &models.SetConfigResponse{Restart: true}, nil
}

View File

@@ -50,6 +50,7 @@ const (
var minioHelpConfigKVMock func(subSys, key string, envOnly bool) (madmin.Help, error)
var minioGetConfigKVMock func(key string) ([]byte, error)
var minioSetConfigKVMock func(kv string) (restart bool, err error)
var minioDelConfigKVMock func(name string) (err error)
// mock function helpConfigKV()
func (ac adminClientMock) helpConfigKV(ctx context.Context, subSys, key string, envOnly bool) (madmin.Help, error) {
@@ -66,6 +67,10 @@ func (ac adminClientMock) setConfigKV(ctx context.Context, kv string) (restart b
return minioSetConfigKVMock(kv)
}
func (ac adminClientMock) delConfigKV(ctx context.Context, name string) (err error) {
return minioDelConfigKVMock(name)
}
func TestListConfig(t *testing.T) {
assert := assert.New(t)
adminClient := adminClientMock{}
@@ -165,6 +170,34 @@ func TestSetConfig(t *testing.T) {
}
func TestDelConfig(t *testing.T) {
assert := assert.New(t)
adminClient := adminClientMock{}
function := "resetConfig()"
// mock function response from setConfig()
minioDelConfigKVMock = func(name string) (err error) {
return nil
}
configName := "region"
ctx := context.Background()
// Test-1 : resetConfig() resets a config with the config name
err := resetConfig(ctx, adminClient, &configName)
if err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
// Test-2 : resetConfig() returns error, handle properly
minioDelConfigKVMock = func(name string) (err error) {
return errors.New("error")
}
err = resetConfig(ctx, adminClient, &configName)
if assert.Error(err) {
assert.Equal("error", err.Error())
}
}
func Test_buildConfig(t *testing.T) {
type args struct {
configName *string

View File

@@ -58,11 +58,12 @@ func registerAdminInfoHandlers(api *operations.ConsoleAPI) {
}
type UsageInfo struct {
Buckets int64
Objects int64
Usage int64
DisksUsage int64
Servers []*models.ServerProperties
Buckets int64
Objects int64
Usage int64
DisksUsage int64
Servers []*models.ServerProperties
EndpointNotReady bool
}
// GetAdminInfo invokes admin info and returns a parsed `UsageInfo` structure
@@ -845,7 +846,12 @@ func getAdminInfoResponse(session *models.Principal, params admin_api.AdminInfoP
}
func getUsageWidgetsForDeployment(prometheusURL string, mAdmin *madmin.AdminClient) (*models.AdminInfoResponse, *models.Error) {
if prometheusURL == "" {
prometheusNotReady := false
if prometheusURL != "" && !testPrometheusURL(prometheusURL) {
prometheusNotReady = true
}
if prometheusURL == "" || prometheusNotReady {
// create a minioClient interface implementation
// defining the client to be used
adminClient := AdminClient{Client: mAdmin}
@@ -858,10 +864,11 @@ func getUsageWidgetsForDeployment(prometheusURL string, mAdmin *madmin.AdminClie
return nil, prepareError(err)
}
sessionResp := &models.AdminInfoResponse{
Buckets: usage.Buckets,
Objects: usage.Objects,
Usage: usage.Usage,
Servers: usage.Servers,
Buckets: usage.Buckets,
Objects: usage.Objects,
Usage: usage.Usage,
Servers: usage.Servers,
PrometheusNotReady: prometheusNotReady,
}
return sessionResp, nil
}

View File

@@ -84,6 +84,7 @@ type MinioAdmin interface {
getConfigKV(ctx context.Context, key string) ([]byte, error)
helpConfigKV(ctx context.Context, subSys, key string, envOnly bool) (madmin.Help, error)
setConfigKV(ctx context.Context, kv string) (restart bool, err error)
delConfigKV(ctx context.Context, kv string) (err error)
serviceRestart(ctx context.Context) error
serverInfo(ctx context.Context) (madmin.InfoMessage, error)
startProfiling(ctx context.Context, profiler madmin.ProfilerType) ([]madmin.StartProfilingResult, error)
@@ -233,6 +234,11 @@ func (ac AdminClient) setConfigKV(ctx context.Context, kv string) (restart bool,
return ac.Client.SetConfigKV(ctx, kv)
}
// implements madmin.DelConfigKV()
func (ac AdminClient) delConfigKV(ctx context.Context, kv string) (err error) {
return ac.Client.DelConfigKV(ctx, kv)
}
// implements madmin.ServiceRestart()
func (ac AdminClient) serviceRestart(ctx context.Context) (err error) {
return ac.Client.ServiceRestart(ctx)

View File

@@ -377,23 +377,36 @@ func newMinioClient(claims *models.Principal) (*minio.Client, error) {
return minioClient, nil
}
// computeObjectURLWithoutEncode returns a MinIO url containing the object filename without encoding
func computeObjectURLWithoutEncode(bucketName, prefix string) (string, error) {
endpoint := getMinIOServer()
u, err := url.Parse(endpoint)
if err != nil {
return "", fmt.Errorf("the provided endpoint is invalid")
}
objectURL := fmt.Sprintf("%s:%s", u.Hostname(), u.Port())
if strings.TrimSpace(bucketName) != "" {
objectURL = path.Join(objectURL, bucketName)
}
if strings.TrimSpace(prefix) != "" {
objectURL = pathJoinFinalSlash(objectURL, prefix)
}
objectURL = fmt.Sprintf("%s://%s", u.Scheme, objectURL)
return objectURL, nil
}
// newS3BucketClient creates a new mc S3Client to talk to the server based on a bucket
func newS3BucketClient(claims *models.Principal, bucketName string, prefix string) (*mc.S3Client, error) {
if claims == nil {
return nil, fmt.Errorf("the provided credentials are invalid")
}
endpoint := getMinIOServer()
u, err := url.Parse(endpoint)
// It's very important to avoid encoding the prefix since the minio client will encode the path itself
objectURL, err := computeObjectURLWithoutEncode(bucketName, prefix)
if err != nil {
return nil, fmt.Errorf("the provided endpoint is invalid")
}
if strings.TrimSpace(bucketName) != "" {
u.Path = path.Join(u.Path, bucketName)
}
if strings.TrimSpace(prefix) != "" {
u.Path = path.Join(u.Path, prefix)
}
s3Config := newS3Config(u.String(), claims.STSAccessKeyID, claims.STSSecretAccessKey, claims.STSSessionToken, false)
s3Config := newS3Config(objectURL, claims.STSAccessKeyID, claims.STSSecretAccessKey, claims.STSSessionToken, false)
client, pErr := mc.S3New(s3Config)
if pErr != nil {
return nil, pErr.Cause
@@ -405,6 +418,16 @@ func newS3BucketClient(claims *models.Principal, bucketName string, prefix strin
return s3Client, nil
}
// pathJoinFinalSlash - like path.Join() but retains trailing slashSeparator of the last element
func pathJoinFinalSlash(elem ...string) string {
if len(elem) > 0 {
if strings.HasSuffix(elem[len(elem)-1], SlashSeparator) {
return path.Join(elem...) + SlashSeparator
}
}
return path.Join(elem...)
}
// newS3Config simply creates a new Config struct using the passed
// parameters.
func newS3Config(endpoint, accessKey, secretKey, sessionToken string, insecure bool) *mc.Config {

90
restapi/client_test.go Normal file
View File

@@ -0,0 +1,90 @@
// This file is part of MinIO Orchestrator
// 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/>.
package restapi
import "testing"
func Test_computeObjectURLWithoutEncode(t *testing.T) {
type args struct {
bucketName string
prefix string
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "http://localhost:9000/bucket-1/小飼弾小飼弾小飼弾.jp",
args: args{
bucketName: "bucket-1",
prefix: "小飼弾小飼弾小飼弾.jpg",
},
want: "http://localhost:9000/bucket-1/小飼弾小飼弾小飼弾.jpg",
wantErr: false,
},
{
name: "http://localhost:9000/bucket-1/a a - a a & a a - a a a.jpg",
args: args{
bucketName: "bucket-1",
prefix: "a a - a a & a a - a a a.jpg",
},
want: "http://localhost:9000/bucket-1/a a - a a & a a - a a a.jpg",
wantErr: false,
},
{
name: "http://localhost:9000/bucket-1/02%20-%20FLY%20ME%20TO%20THE%20MOON%20.jpg",
args: args{
bucketName: "bucket-1",
prefix: "02%20-%20FLY%20ME%20TO%20THE%20MOON%20.jpg",
},
want: "http://localhost:9000/bucket-1/02%20-%20FLY%20ME%20TO%20THE%20MOON%20.jpg",
wantErr: false,
},
{
name: "http://localhost:9000/bucket-1/!@#$%^&*()_+.jpg",
args: args{
bucketName: "bucket-1",
prefix: "!@#$%^&*()_+.jpg",
},
want: "http://localhost:9000/bucket-1/!@#$%^&*()_+.jpg",
wantErr: false,
},
{
name: "http://localhost:9000/bucket-1/test/test2/小飼弾小飼弾小飼弾.jpg",
args: args{
bucketName: "bucket-1",
prefix: "test/test2/小飼弾小飼弾小飼弾.jpg",
},
want: "http://localhost:9000/bucket-1/test/test2/小飼弾小飼弾小飼弾.jpg",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := computeObjectURLWithoutEncode(tt.args.bucketName, tt.args.prefix)
if (err != nil) != tt.wantErr {
t.Errorf("computeObjectURLWithoutEncode() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("computeObjectURLWithoutEncode() got = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -54,4 +54,5 @@ const (
ConsoleLogQueryURL = "CONSOLE_LOG_QUERY_URL"
ConsoleLogQueryAuthToken = "CONSOLE_LOG_QUERY_AUTH_TOKEN"
LogSearchQueryAuthToken = "LOGSEARCH_QUERY_AUTH_TOKEN"
SlashSeparator = "/"
)

View File

@@ -2099,6 +2099,37 @@ func init() {
}
}
},
"/configs/{name}/reset": {
"get": {
"tags": [
"AdminAPI"
],
"summary": "Configuration reset",
"operationId": "ResetConfig",
"parameters": [
{
"type": "string",
"name": "name",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/setConfigResponse"
}
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
}
},
"/group": {
"get": {
"tags": [
@@ -3624,6 +3655,9 @@ func init() {
"objects": {
"type": "integer"
},
"prometheusNotReady": {
"type": "boolean"
},
"servers": {
"type": "array",
"items": {
@@ -7796,6 +7830,37 @@ func init() {
}
}
},
"/configs/{name}/reset": {
"get": {
"tags": [
"AdminAPI"
],
"summary": "Configuration reset",
"operationId": "ResetConfig",
"parameters": [
{
"type": "string",
"name": "name",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/setConfigResponse"
}
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
}
},
"/group": {
"get": {
"tags": [
@@ -9441,6 +9506,9 @@ func init() {
"objects": {
"type": "integer"
},
"prometheusNotReady": {
"type": "boolean"
},
"servers": {
"type": "array",
"items": {

View File

@@ -0,0 +1,88 @@
// Code generated by go-swagger; DO NOT EDIT.
// 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/>.
//
package admin_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"net/http"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
)
// ResetConfigHandlerFunc turns a function with the right signature into a reset config handler
type ResetConfigHandlerFunc func(ResetConfigParams, *models.Principal) middleware.Responder
// Handle executing the request and returning a response
func (fn ResetConfigHandlerFunc) Handle(params ResetConfigParams, principal *models.Principal) middleware.Responder {
return fn(params, principal)
}
// ResetConfigHandler interface for that can handle valid reset config params
type ResetConfigHandler interface {
Handle(ResetConfigParams, *models.Principal) middleware.Responder
}
// NewResetConfig creates a new http.Handler for the reset config operation
func NewResetConfig(ctx *middleware.Context, handler ResetConfigHandler) *ResetConfig {
return &ResetConfig{Context: ctx, Handler: handler}
}
/* ResetConfig swagger:route GET /configs/{name}/reset AdminAPI resetConfig
Configuration reset
*/
type ResetConfig struct {
Context *middleware.Context
Handler ResetConfigHandler
}
func (o *ResetConfig) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
route, rCtx, _ := o.Context.RouteInfo(r)
if rCtx != nil {
*r = *rCtx
}
var Params = NewResetConfigParams()
uprinc, aCtx, err := o.Context.Authorize(r, route)
if err != nil {
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
if aCtx != nil {
*r = *aCtx
}
var principal *models.Principal
if uprinc != nil {
principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise
}
if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
res := o.Handler.Handle(Params, principal) // actually handle the request
o.Context.Respond(rw, r, route.Produces, route, res)
}

View File

@@ -0,0 +1,88 @@
// Code generated by go-swagger; DO NOT EDIT.
// 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/>.
//
package admin_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/strfmt"
)
// NewResetConfigParams creates a new ResetConfigParams object
//
// There are no default values defined in the spec.
func NewResetConfigParams() ResetConfigParams {
return ResetConfigParams{}
}
// ResetConfigParams contains all the bound params for the reset config operation
// typically these are obtained from a http.Request
//
// swagger:parameters ResetConfig
type ResetConfigParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
/*
Required: true
In: path
*/
Name string
}
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
// for simple values it will use straight method calls.
//
// To ensure default values, the struct must have been initialized with NewResetConfigParams() beforehand.
func (o *ResetConfigParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
var res []error
o.HTTPRequest = r
rName, rhkName, _ := route.Params.GetOK("name")
if err := o.bindName(rName, rhkName, route.Formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
// bindName binds and validates parameter Name from path.
func (o *ResetConfigParams) bindName(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: true
// Parameter is provided by construction from the route
o.Name = raw
return nil
}

View File

@@ -0,0 +1,133 @@
// Code generated by go-swagger; DO NOT EDIT.
// 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/>.
//
package admin_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
"github.com/go-openapi/runtime"
"github.com/minio/console/models"
)
// ResetConfigOKCode is the HTTP code returned for type ResetConfigOK
const ResetConfigOKCode int = 200
/*ResetConfigOK A successful response.
swagger:response resetConfigOK
*/
type ResetConfigOK struct {
/*
In: Body
*/
Payload *models.SetConfigResponse `json:"body,omitempty"`
}
// NewResetConfigOK creates ResetConfigOK with default headers values
func NewResetConfigOK() *ResetConfigOK {
return &ResetConfigOK{}
}
// WithPayload adds the payload to the reset config o k response
func (o *ResetConfigOK) WithPayload(payload *models.SetConfigResponse) *ResetConfigOK {
o.Payload = payload
return o
}
// SetPayload sets the payload to the reset config o k response
func (o *ResetConfigOK) SetPayload(payload *models.SetConfigResponse) {
o.Payload = payload
}
// WriteResponse to the client
func (o *ResetConfigOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(200)
if o.Payload != nil {
payload := o.Payload
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
}
/*ResetConfigDefault Generic error response.
swagger:response resetConfigDefault
*/
type ResetConfigDefault struct {
_statusCode int
/*
In: Body
*/
Payload *models.Error `json:"body,omitempty"`
}
// NewResetConfigDefault creates ResetConfigDefault with default headers values
func NewResetConfigDefault(code int) *ResetConfigDefault {
if code <= 0 {
code = 500
}
return &ResetConfigDefault{
_statusCode: code,
}
}
// WithStatusCode adds the status to the reset config default response
func (o *ResetConfigDefault) WithStatusCode(code int) *ResetConfigDefault {
o._statusCode = code
return o
}
// SetStatusCode sets the status to the reset config default response
func (o *ResetConfigDefault) SetStatusCode(code int) {
o._statusCode = code
}
// WithPayload adds the payload to the reset config default response
func (o *ResetConfigDefault) WithPayload(payload *models.Error) *ResetConfigDefault {
o.Payload = payload
return o
}
// SetPayload sets the payload to the reset config default response
func (o *ResetConfigDefault) SetPayload(payload *models.Error) {
o.Payload = payload
}
// WriteResponse to the client
func (o *ResetConfigDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(o._statusCode)
if o.Payload != nil {
payload := o.Payload
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
}

View File

@@ -0,0 +1,116 @@
// Code generated by go-swagger; DO NOT EDIT.
// 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/>.
//
package admin_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"errors"
"net/url"
golangswaggerpaths "path"
"strings"
)
// ResetConfigURL generates an URL for the reset config operation
type ResetConfigURL struct {
Name string
_basePath string
// avoid unkeyed usage
_ struct{}
}
// WithBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *ResetConfigURL) WithBasePath(bp string) *ResetConfigURL {
o.SetBasePath(bp)
return o
}
// SetBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *ResetConfigURL) SetBasePath(bp string) {
o._basePath = bp
}
// Build a url path and query string
func (o *ResetConfigURL) Build() (*url.URL, error) {
var _result url.URL
var _path = "/configs/{name}/reset"
name := o.Name
if name != "" {
_path = strings.Replace(_path, "{name}", name, -1)
} else {
return nil, errors.New("name is required on ResetConfigURL")
}
_basePath := o._basePath
if _basePath == "" {
_basePath = "/api/v1"
}
_result.Path = golangswaggerpaths.Join(_basePath, _path)
return &_result, nil
}
// Must is a helper function to panic when the url builder returns an error
func (o *ResetConfigURL) Must(u *url.URL, err error) *url.URL {
if err != nil {
panic(err)
}
if u == nil {
panic("url can't be nil")
}
return u
}
// String returns the string representation of the path with query string
func (o *ResetConfigURL) String() string {
return o.Must(o.Build()).String()
}
// BuildFull builds a full url with scheme, host, path and query string
func (o *ResetConfigURL) BuildFull(scheme, host string) (*url.URL, error) {
if scheme == "" {
return nil, errors.New("scheme is required for a full url on ResetConfigURL")
}
if host == "" {
return nil, errors.New("host is required for a full url on ResetConfigURL")
}
base, err := o.Build()
if err != nil {
return nil, err
}
base.Scheme = scheme
base.Host = host
return base, nil
}
// StringFull returns the string representation of a complete url
func (o *ResetConfigURL) StringFull(scheme, host string) string {
return o.Must(o.BuildFull(scheme, host)).String()
}

View File

@@ -311,6 +311,9 @@ func NewConsoleAPI(spec *loads.Document) *ConsoleAPI {
AdminAPIRemoveUserHandler: admin_api.RemoveUserHandlerFunc(func(params admin_api.RemoveUserParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation admin_api.RemoveUser has not yet been implemented")
}),
AdminAPIResetConfigHandler: admin_api.ResetConfigHandlerFunc(func(params admin_api.ResetConfigParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation admin_api.ResetConfig has not yet been implemented")
}),
AdminAPIRestartServiceHandler: admin_api.RestartServiceHandlerFunc(func(params admin_api.RestartServiceParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation admin_api.RestartService has not yet been implemented")
}),
@@ -581,6 +584,8 @@ type ConsoleAPI struct {
AdminAPIRemovePolicyHandler admin_api.RemovePolicyHandler
// AdminAPIRemoveUserHandler sets the operation handler for the remove user operation
AdminAPIRemoveUserHandler admin_api.RemoveUserHandler
// AdminAPIResetConfigHandler sets the operation handler for the reset config operation
AdminAPIResetConfigHandler admin_api.ResetConfigHandler
// AdminAPIRestartServiceHandler sets the operation handler for the restart service operation
AdminAPIRestartServiceHandler admin_api.RestartServiceHandler
// UserAPISessionCheckHandler sets the operation handler for the session check operation
@@ -948,6 +953,9 @@ func (o *ConsoleAPI) Validate() error {
if o.AdminAPIRemoveUserHandler == nil {
unregistered = append(unregistered, "admin_api.RemoveUserHandler")
}
if o.AdminAPIResetConfigHandler == nil {
unregistered = append(unregistered, "admin_api.ResetConfigHandler")
}
if o.AdminAPIRestartServiceHandler == nil {
unregistered = append(unregistered, "admin_api.RestartServiceHandler")
}
@@ -1429,6 +1437,10 @@ func (o *ConsoleAPI) initHandlerCache() {
o.handlers["DELETE"] = make(map[string]http.Handler)
}
o.handlers["DELETE"]["/user"] = admin_api.NewRemoveUser(o.context, o.AdminAPIRemoveUserHandler)
if o.handlers["GET"] == nil {
o.handlers["GET"] = make(map[string]http.Handler)
}
o.handlers["GET"]["/configs/{name}/reset"] = admin_api.NewResetConfig(o.context, o.AdminAPIResetConfigHandler)
if o.handlers["POST"] == nil {
o.handlers["POST"] = make(map[string]http.Handler)
}

View File

@@ -898,9 +898,6 @@ func getBucketRewindResponse(session *models.Principal, params user_api.GetBucke
Name: name,
}
cont, _ := json.Marshal(content)
fmt.Println(string(cont))
rewindItems = append(rewindItems, listElement)
}

View File

@@ -77,76 +77,35 @@ func registerObjectsHandlers(api *operations.ConsoleAPI) {
})
// download object
api.UserAPIDownloadObjectHandler = user_api.DownloadObjectHandlerFunc(func(params user_api.DownloadObjectParams, session *models.Principal) middleware.Responder {
isPreview := *params.Preview
resp, err := getDownloadObjectResponse(session, params)
isFolder := false
var prefix string
if params.Prefix != "" {
encodedPrefix := SanitizeEncodedPrefix(params.Prefix)
decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
if err != nil {
return user_api.NewDownloadObjectDefault(int(400)).WithPayload(prepareError(err))
}
prefix = string(decodedPrefix)
}
folders := strings.Split(prefix, "/")
if folders[len(folders)-1] == "" {
isFolder = true
}
var resp middleware.Responder
var err *models.Error
if isFolder {
resp, err = getDownloadFolderResponse(session, params)
} else {
resp, err = getDownloadObjectResponse(session, params)
}
if err != nil {
return user_api.NewDownloadObjectDefault(int(err.Code)).WithPayload(err)
}
return middleware.ResponderFunc(func(rw http.ResponseWriter, _ runtime.Producer) {
defer resp.Close()
// indicate it's a download / inline content to the browser, and the size of the object
var prefixPath string
var filename string
if params.Prefix != "" {
encodedPrefix := SanitizeEncodedPrefix(params.Prefix)
decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
if err != nil {
log.Println(err)
}
prefixPath = string(decodedPrefix)
}
prefixElements := strings.Split(prefixPath, "/")
isFolder := false
if len(prefixElements) > 0 {
if prefixElements[len(prefixElements)-1] == "" {
filename = prefixElements[len(prefixElements)-2]
isFolder = true
} else {
filename = prefixElements[len(prefixElements)-1]
}
}
if isPreview {
rw.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", filename))
rw.Header().Set("X-Frame-Options", "SAMEORIGIN")
rw.Header().Set("X-XSS-Protection", "1")
} else if isFolder {
rw.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.zip\"", filename))
rw.Header().Set("Content-Type", "application/zip")
} else {
rw.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
rw.Header().Set("Content-Type", "application/octet-stream")
}
// indicate object size & content type
if !isFolder {
stat, err := resp.(*minio.Object).Stat()
if err != nil {
log.Println(err)
} else {
rw.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size))
contentType := stat.ContentType
if isPreview {
// In case content type was uploaded as octet-stream, we double verify content type
if stat.ContentType == "application/octet-stream" {
contentType = mimedb.TypeByExtension(filepath.Ext(filename))
}
}
rw.Header().Set("Content-Type", contentType)
}
}
// Copy the stream
_, err := io.Copy(rw, resp)
if err != nil {
log.Println(err)
}
})
return resp
})
// upload object
api.UserAPIPostBucketsBucketNameObjectsUploadHandler = user_api.PostBucketsBucketNameObjectsUploadHandlerFunc(func(params user_api.PostBucketsBucketNameObjectsUploadParams, session *models.Principal) middleware.Responder {
@@ -315,7 +274,128 @@ func listBucketObjects(ctx context.Context, client MinioClient, bucketName strin
return objects, nil
}
func getDownloadObjectResponse(session *models.Principal, params user_api.DownloadObjectParams) (io.ReadCloser, *models.Error) {
func getDownloadObjectResponse(session *models.Principal, params user_api.DownloadObjectParams) (middleware.Responder, *models.Error) {
ctx := context.Background()
var prefix string
mClient, err := newMinioClient(session)
if err != nil {
return nil, prepareError(err)
}
if params.Prefix != "" {
encodedPrefix := SanitizeEncodedPrefix(params.Prefix)
decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
if err != nil {
return nil, prepareError(err)
}
prefix = string(decodedPrefix)
}
resp, err := mClient.GetObject(ctx, params.BucketName, prefix, minio.GetObjectOptions{})
if err != nil {
return nil, prepareError(err)
}
return middleware.ResponderFunc(func(rw http.ResponseWriter, _ runtime.Producer) {
defer resp.Close()
// indicate object size & content type
stat, err := resp.Stat()
statOk := false
if err != nil {
log.Println(err)
} else {
statOk = true
}
isPreview := params.Preview != nil && *params.Preview
// indicate it's a download / inline content to the browser, and the size of the object
var prefixPath string
var filename string
if params.Prefix != "" {
encodedPrefix := SanitizeEncodedPrefix(params.Prefix)
decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
if err != nil {
log.Println(err)
}
prefixPath = string(decodedPrefix)
}
prefixElements := strings.Split(prefixPath, "/")
if len(prefixElements) > 0 {
if prefixElements[len(prefixElements)-1] == "" {
filename = prefixElements[len(prefixElements)-2]
} else {
filename = prefixElements[len(prefixElements)-1]
}
}
// if we are getting a Range Request (video) handle that specially
isRange := params.HTTPRequest.Header.Get("Range")
if isRange != "" {
rangeFrom := -1
rangeTo := -1
parts := strings.Split(isRange, "=")
if len(parts) > 1 {
rangeParts := strings.Split(parts[1], "-")
var err error
rangeFrom, err = strconv.Atoi(rangeParts[0])
if err != nil {
log.Println(err)
return
}
if rangeParts[1] != "" {
rangeTo, err = strconv.Atoi(rangeParts[1])
if err != nil {
log.Println(err)
return
}
}
}
if handleRangeRequest(rw, isRange, stat, isPreview, filename, resp, params, rangeTo, rangeFrom) {
return
}
}
if isPreview {
rw.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", filename))
rw.Header().Set("X-Frame-Options", "SAMEORIGIN")
rw.Header().Set("X-XSS-Protection", "1")
} else {
rw.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
rw.Header().Set("Content-Type", "application/octet-stream")
}
// indicate object size & content type
if statOk {
rw.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size))
contentType := stat.ContentType
if isPreview {
// In case content type was uploaded as octet-stream, we double verify content type
if stat.ContentType == "application/octet-stream" {
contentType = mimedb.TypeByExtension(filepath.Ext(filename))
}
}
rw.Header().Set("Content-Type", contentType)
}
// Copy the stream
_, err = io.Copy(rw, resp)
if err != nil {
log.Println(err)
}
}), nil
}
func getDownloadFolderResponse(session *models.Principal, params user_api.DownloadObjectParams) (middleware.Responder, *models.Error) {
ctx := context.Background()
var prefix string
mClient, err := newMinioClient(session)
@@ -327,49 +407,73 @@ func getDownloadObjectResponse(session *models.Principal, params user_api.Downlo
}
prefix = string(decodedPrefix)
}
isFolder := false
folders := strings.Split(prefix, "/")
if folders[len(folders)-1] == "" {
isFolder = true
}
if isFolder {
if err != nil {
return nil, prepareError(err)
}
minioClient := minioClient{client: mClient}
objects, err := listBucketObjects(ctx, minioClient, params.BucketName, prefix, true, false, false)
if err != nil {
return nil, prepareError(err)
}
w := new(bytes.Buffer)
zipw := zip.NewWriter(w)
var folder string
if len(folders) > 1 {
folder = folders[len(folders)-2]
}
for i := 0; i < len(objects); i++ {
name := folder + objects[i].Name[len(prefix)-1:]
object, err := mClient.GetObject(ctx, params.BucketName, objects[i].Name, minio.GetObjectOptions{})
if err != nil {
return nil, prepareError(err)
}
f, err := zipw.Create(name)
if err != nil {
return nil, prepareError(err)
}
buf := new(bytes.Buffer)
buf.ReadFrom(object)
f.Write(buf.Bytes())
}
zipw.Close()
zipfile := io.NopCloser(bytes.NewReader(w.Bytes()))
return zipfile, nil
}
object, err := mClient.GetObject(ctx, params.BucketName, prefix, minio.GetObjectOptions{})
if err != nil {
return nil, prepareError(err)
}
return object, nil
minioClient := minioClient{client: mClient}
objects, err := listBucketObjects(ctx, minioClient, params.BucketName, prefix, true, false, false)
if err != nil {
return nil, prepareError(err)
}
w := new(bytes.Buffer)
zipw := zip.NewWriter(w)
var folder string
if len(folders) > 1 {
folder = folders[len(folders)-2]
}
for i := 0; i < len(objects); i++ {
name := folder + objects[i].Name[len(prefix)-1:]
object, err := mClient.GetObject(ctx, params.BucketName, objects[i].Name, minio.GetObjectOptions{})
if err != nil {
return nil, prepareError(err)
}
f, err := zipw.Create(name)
if err != nil {
return nil, prepareError(err)
}
buf := new(bytes.Buffer)
buf.ReadFrom(object)
f.Write(buf.Bytes())
}
zipw.Close()
resp := io.NopCloser(bytes.NewReader(w.Bytes()))
return middleware.ResponderFunc(func(rw http.ResponseWriter, _ runtime.Producer) {
defer resp.Close()
// indicate it's a download / inline content to the browser, and the size of the object
var prefixPath string
var filename string
if params.Prefix != "" {
encodedPrefix := SanitizeEncodedPrefix(params.Prefix)
decodedPrefix, err := base64.StdEncoding.DecodeString(encodedPrefix)
if err != nil {
log.Println(err)
}
prefixPath = string(decodedPrefix)
}
prefixElements := strings.Split(prefixPath, "/")
if len(prefixElements) > 0 {
if prefixElements[len(prefixElements)-1] == "" {
filename = prefixElements[len(prefixElements)-2]
} else {
filename = prefixElements[len(prefixElements)-1]
}
}
rw.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.zip\"", filename))
rw.Header().Set("Content-Type", "application/zip")
// Copy the stream
_, err := io.Copy(rw, resp)
if err != nil {
log.Println(err)
}
}), nil
}
// getDeleteObjectResponse returns whether there was an error on deletion of object
@@ -929,3 +1033,84 @@ func getHost(authority string) (host string) {
}
return authority
}
func handleRangeRequest(rw http.ResponseWriter, isRange string, stat minio.ObjectInfo, isPreview bool, filename string, resp *minio.Object, params user_api.DownloadObjectParams, rangeTo int, rangeFrom int) bool {
parts := strings.Split(isRange, "=")
if len(parts) > 1 {
if parts[1] == "0-1" {
contentType := stat.ContentType
if isPreview {
// In case content type was uploaded as octet-stream, we double verify content type
if stat.ContentType == "application/octet-stream" {
contentType = mimedb.TypeByExtension(filepath.Ext(filename))
}
}
rw.Header().Set("Content-Type", contentType)
rw.Header().Set("Content-Length", "2")
rw.Header().Set("Content-Range", fmt.Sprintf("bytes 0-1/%d", stat.Size))
rw.Header().Set("Accept-Ranges", "bytes")
rw.Header().Set("Access-Control-Allow-Origin", "*")
rw.WriteHeader(206)
byts := make([]byte, 2)
t, err := resp.Read(byts)
log.Println("read", t, "bytes")
if err != nil {
log.Println(err)
}
rw.Write(byts)
return true
}
contentType := stat.ContentType
if isPreview {
// In case content type was uploaded as octet-stream, we double verify content type
if stat.ContentType == "application/octet-stream" {
contentType = mimedb.TypeByExtension(filepath.Ext(filename))
}
}
rw.Header().Set("Content-Type", contentType)
isFirefox := false
if strings.Contains(params.HTTPRequest.UserAgent(), "Firefox") {
isFirefox = true
}
if !isFirefox {
rw.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size))
}
if rangeTo > -1 {
rw.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", rangeFrom, rangeTo, stat.Size))
if isFirefox {
rw.Header().Set("Content-Length", fmt.Sprintf("%d", rangeTo-rangeFrom+1))
}
} else {
rw.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", rangeFrom, stat.Size-1, stat.Size))
if isFirefox {
rw.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size-int64(rangeFrom)))
}
}
rw.Header().Set("Accept-Ranges", "bytes")
rw.Header().Set("Access-Control-Allow-Origin", "*")
rw.WriteHeader(206)
if rangeTo > -1 {
byts := make([]byte, rangeTo+1)
t, err := resp.ReadAt(byts, int64(rangeFrom))
log.Println("0 read", t, "bytes")
if err != nil {
log.Println(err)
}
rw.Write(byts)
} else {
byts := make([]byte, stat.Size-int64(rangeFrom))
t, err := resp.ReadAt(byts, int64(rangeFrom))
log.Println("1 read", t, "bytes")
if err != nil {
log.Println(err)
}
rw.Write(byts)
}
}
return false
}

View File

@@ -1932,6 +1932,27 @@ paths:
tags:
- AdminAPI
/configs/{name}/reset:
get:
summary: Configuration reset
operationId: ResetConfig
parameters:
- name: name
in: path
required: true
type: string
responses:
200:
description: A successful response.
schema:
$ref: "#/definitions/setConfigResponse"
default:
description: Generic error response.
schema:
$ref: "#/definitions/error"
tags:
- AdminAPI
/service/restart:
post:
summary: Restart Service
@@ -2643,7 +2664,7 @@ definitions:
name:
type: array
items:
type: string
type: string
entityType:
$ref: "#/definitions/policyEntity"
entityName:
@@ -3207,6 +3228,8 @@ definitions:
type: integer
usage:
type: integer
prometheusNotReady:
type: boolean
widgets:
type: array
items:
@@ -3893,7 +3916,6 @@ definitions:
items:
$ref: "#/definitions/iamPolicyStatement"
iamPolicyStatement:
type: object
properties: