Split Object Browser and Buckets Admin (#2500)

Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Daniel Valdivia
2022-12-14 16:20:13 -08:00
committed by GitHub
parent 0c4a73ff10
commit f557c4c550
34 changed files with 1097 additions and 1097 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -20,7 +20,6 @@
"@types/react-dom": "18.0.9",
"@types/react-grid-layout": "^1.1.1",
"@types/react-redux": "^7.1.24",
"@types/react-router-dom": "^5.3.3",
"@types/react-virtualized": "^9.21.21",
"@types/superagent": "^4.1.16",
"@types/webpack-env": "^1.14.1",
@@ -79,7 +78,7 @@
"@types/recharts": "^1.8.24",
"prettier": "2.7.1",
"react-scripts": "5.0.1",
"testcafe": "^1.18.6",
"testcafe": "^2.1.0",
"typescript": "^4.4.3"
},
"resolutions": {

View File

@@ -146,7 +146,7 @@ test("Can Browse Bucket", () => {
expect(
hasPermission(
"bucket-svc",
IAM_PAGES_PERMISSIONS[IAM_PAGES.BUCKETS_BROWSE_VIEW]
IAM_PAGES_PERMISSIONS[IAM_PAGES.OBJECT_BROWSER_VIEW]
)
).toBe(true);
});
@@ -166,7 +166,7 @@ test("Can browse a bucket for a policy with a wildcard", () => {
expect(
hasPermission(
"testbucket-0",
IAM_PAGES_PERMISSIONS[IAM_PAGES.BUCKETS_BROWSE_VIEW]
IAM_PAGES_PERMISSIONS[IAM_PAGES.OBJECT_BROWSER_VIEW]
)
).toBe(true);
});

View File

@@ -117,7 +117,10 @@ export const IAM_PAGES = {
BUCKETS: "/buckets",
ADD_BUCKETS: "add-bucket",
BUCKETS_ADMIN_VIEW: ":bucketName/admin/*",
BUCKETS_BROWSE_VIEW: ":bucketName/browse/*",
/* Object Browser */
OBJECT_BROWSER_VIEW: "/browser",
OBJECT_BROWSER_BUCKET_VIEW: "/browser/:bucketName",
OBJECT_BROWSER_BUCKET_DETAILS_VIEW: "/browser/:bucketName/*",
/* Identity */
IDENTITY: "/identity",
USERS: "/identity/users",
@@ -307,7 +310,7 @@ export const IAM_PAGES_PERMISSIONS = {
[IAM_PAGES.BUCKETS_ADMIN_VIEW]: [
...IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN], // bucket admin page
],
[IAM_PAGES.BUCKETS_BROWSE_VIEW]: [
[IAM_PAGES.OBJECT_BROWSER_VIEW]: [
...IAM_PERMISSIONS[IAM_ROLES.BUCKET_OWNER],
...IAM_PERMISSIONS[IAM_ROLES.BUCKET_VIEWER],
],

View File

@@ -39,6 +39,7 @@ import BackLink from "../../../../common/BackLink";
import {
newMessage,
resetMessages,
setIsOpeningOD,
setIsVersioned,
setLoadingLocking,
setLoadingObjectInfo,
@@ -184,24 +185,23 @@ const BrowserHandler = () => {
const loadingLocking = useSelector(
(state: AppState) => state.objectBrowser.loadingLocking
);
const bucketToRewind = useSelector(
(state: AppState) => state.objectBrowser.rewind.bucketToRewind
);
const loadRecords = useSelector(
(state: AppState) => state.objectBrowser.loadRecords
);
const detailsOpen = useSelector(
(state: AppState) => state.objectBrowser.objectDetailsOpen
);
const selectedInternalPaths = useSelector(
(state: AppState) => state.objectBrowser.selectedInternalPaths
);
const simplePath = useSelector(
(state: AppState) => state.objectBrowser.simplePath
);
const isOpeningOD = useSelector(
(state: AppState) => state.objectBrowser.isOpeningObjectDetail
);
const features = useSelector(selFeatures);
const bucketName = params.bucketName || "";
const pathSegment = location.pathname.split("/browse/");
const pathSegment = location.pathname.split(`/browser/${bucketName}/`);
const internalPaths = pathSegment.length === 2 ? pathSegment[1] : "";
const obOnly = !!features?.includes("object-browser-only");
@@ -226,9 +226,12 @@ const BrowserHandler = () => {
// Session expired.
window.location.reload();
} else if (response.error === "Access Denied.") {
const internalPathsPrefix = response.prefix;
let pathPrefix = "";
if (internalPaths) {
const decodedPath = decodeURLString(internalPaths);
if (internalPathsPrefix) {
const decodedPath = decodeURLString(internalPathsPrefix);
pathPrefix = decodedPath.endsWith("/")
? decodedPath
: decodedPath + "/";
@@ -266,7 +269,7 @@ const BrowserHandler = () => {
}
}
},
[dispatch, internalPaths, allowResources, bucketName]
[dispatch, allowResources, bucketName]
);
const initWSRequest = useCallback(
@@ -317,54 +320,63 @@ const BrowserHandler = () => {
}, []);
useEffect(() => {
if (objectsWS?.readyState === 1) {
const decodedIPaths = decodeURLString(internalPaths);
const decodedIPaths = decodeURLString(internalPaths);
if (decodedIPaths.endsWith("/") || decodedIPaths === "") {
dispatch(setObjectDetailsView(false));
dispatch(setSelectedObjectView(null));
dispatch(
setSimplePathHandler(decodedIPaths === "" ? "/" : decodedIPaths)
);
} else {
dispatch(setLoadingObjectInfo(true));
dispatch(setObjectDetailsView(true));
dispatch(setLoadingVersions(true));
dispatch(
setSelectedObjectView(
`${decodedIPaths ? `${encodeURLString(decodedIPaths)}` : ``}`
)
);
dispatch(
setSimplePathHandler(
`${decodedIPaths.split("/").slice(0, -1).join("/")}/`
)
);
}
dispatch(setLoadingVersioning(true));
if (decodedIPaths.endsWith("/") || decodedIPaths === "") {
dispatch(setObjectDetailsView(false));
dispatch(setSelectedObjectView(null));
dispatch(
setSimplePathHandler(decodedIPaths === "" ? "/" : decodedIPaths)
);
} else {
dispatch(setLoadingObjectInfo(true));
dispatch(setObjectDetailsView(true));
dispatch(setLoadingVersions(true));
dispatch(
setSelectedObjectView(
`${decodedIPaths ? `${encodeURLString(decodedIPaths)}` : ``}`
)
);
dispatch(
setSimplePathHandler(
`${decodedIPaths.split("/").slice(0, -1).join("/")}/`
)
);
}
}, [internalPaths, rewindDate, rewindEnabled, dispatch]);
// Direct file access effect / prefix
useEffect(() => {
if (!loadingObjects && loadRecords && !rewindEnabled) {
const parentPath = `${decodeURLString(internalPaths)
.split("/")
.slice(0, -1)
.join("/")}/`;
if (!loadingObjects && !loadRecords && !rewindEnabled && !isOpeningOD) {
// No requests are in progress, We review current path, if it doesn't end in '/' and current list is empty then we trigger a new request.
const decodedInternalPaths = decodeURLString(internalPaths);
initWSRequest(parentPath, new Date());
if (
!decodedInternalPaths.endsWith("/") &&
simplePath !== decodedInternalPaths &&
decodedInternalPaths !== ""
) {
setLoadingRecords(true);
const parentPath = `${decodedInternalPaths
.split("/")
.slice(0, -1)
.join("/")}/`;
initWSRequest(parentPath, new Date());
}
}
dispatch(setIsOpeningOD(false));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
loadingObjects,
loadRecords,
bucketName,
bucketToRewind,
dispatch,
internalPaths,
rewindDate,
rewindEnabled,
initWSRequest,
detailsOpen,
rewindEnabled,
simplePath,
]);
const displayListObjects = hasPermission(bucketName, [
@@ -547,7 +559,12 @@ const BrowserHandler = () => {
<Fragment>
{!obOnly ? (
<PageHeader
label={<BackLink label={"Buckets"} to={IAM_PAGES.BUCKETS} />}
label={
<BackLink
label={"Object Browser"}
to={IAM_PAGES.OBJECT_BROWSER_VIEW}
/>
}
actions={
<SecureComponent
scopes={IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN]}

View File

@@ -200,10 +200,6 @@ const BucketDetails = ({ classes }: IBucketDetailsProps) => {
}
};
const openBucketBrowser = () => {
navigate(`/buckets/${bucketName}/browse`);
};
return (
<Fragment>
{deleteOpen && (
@@ -231,7 +227,9 @@ const BucketDetails = ({ classes }: IBucketDetailsProps) => {
<Button
id={"switch-browse-view"}
aria-label="Browse Bucket"
onClick={openBucketBrowser}
onClick={() => {
navigate(`/browser/${bucketName}`);
}}
icon={
<FolderIcon style={{ width: 20, height: 20, marginTop: -3 }} />
}

View File

@@ -23,9 +23,6 @@ import { IAM_PAGES } from "../../../common/SecureComponent/permissions";
const ListBuckets = React.lazy(() => import("./ListBuckets/ListBuckets"));
const BucketDetails = React.lazy(() => import("./BucketDetails/BucketDetails"));
const BrowserHandler = React.lazy(
() => import("./BucketDetails/BrowserHandler")
);
const AddBucket = React.lazy(() => import("./ListBuckets/AddBucket/AddBucket"));
const Buckets = () => {
@@ -56,22 +53,6 @@ const Buckets = () => {
</Suspense>
}
/>
<Route
path=":bucketName/browse/*"
element={
<Suspense fallback={<LoadingComponent />}>
<BrowserHandler />
</Suspense>
}
/>
<Route
path=":bucketName/browse"
element={
<Suspense fallback={<LoadingComponent />}>
<BrowserHandler />
</Suspense>
}
/>
<Route element={<Navigate to={`/buckets`} />} path="*" />
<Route

View File

@@ -16,14 +16,10 @@
import React, { Fragment } from "react";
import get from "lodash/get";
import { Theme } from "@mui/material/styles";
import { Button } from "mds";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import {
ArrowRightIcon,
BucketsIcon,
ReportedUsageIcon,
SettingsIcon,
TotalObjectsIcon,
} from "../../../../icons";
import { Bucket } from "../types";
@@ -38,14 +34,12 @@ import { Link, useNavigate } from "react-router-dom";
import {
IAM_PERMISSIONS,
IAM_ROLES,
permissionTooltipHelper,
} from "../../../../common/SecureComponent/permissions";
import { SecureComponent } from "../../../../common/SecureComponent";
import clsx from "clsx";
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
import { hasPermission } from "../../../../common/SecureComponent";
import clsx from "clsx";
import makeStyles from "@mui/styles/makeStyles";
const styles = (theme: Theme) =>
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
marginBottom: 30,
@@ -53,6 +47,10 @@ const styles = (theme: Theme) =>
color: theme.palette.primary.main,
border: "#E5E5E5 1px solid",
borderRadius: 2,
textDecoration: "none",
"&:hover": {
backgroundColor: "#fafafa",
},
"& .min-icon": {
height: 14,
width: 14,
@@ -161,11 +159,14 @@ const styles = (theme: Theme) =>
marginTop: "-33px",
},
},
});
disabled: {
backgroundColor: "red",
},
})
);
interface IBucketListItem {
bucket: Bucket;
classes: any;
onSelect: (e: React.ChangeEvent<HTMLInputElement>) => void;
selected: boolean;
bulkSelect: boolean;
@@ -173,13 +174,13 @@ interface IBucketListItem {
}
const BucketListItem = ({
classes,
bucket,
onSelect,
selected,
bulkSelect,
noManage = false,
}: IBucketListItem) => {
const classes = useStyles();
const navigate = useNavigate();
const usage = niceBytes(`${bucket.size}` || "0");
@@ -189,10 +190,9 @@ const BucketListItem = ({
const quota = get(bucket, "details.quota.quota", "0");
const quotaForString = calculateBytes(quota, true, false);
const manageAllowed = hasPermission(
bucket.name,
IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN]
);
const manageAllowed =
hasPermission(bucket.name, IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN]) &&
false;
const accessToStr = (bucket: Bucket): string => {
if (bucket.rw_access?.read && !bucket.rw_access?.write) {
@@ -209,7 +209,19 @@ const BucketListItem = ({
};
return (
<Grid container className={clsx(classes.root, "bucket-item")}>
<Grid
container
className={clsx(classes.root, "bucket-item", {
[classes.disabled]: manageAllowed,
})}
onClick={() => {
navigate(`/buckets/${bucket.name}/admin`);
}}
sx={{
cursor: "pointer",
}}
id={`manageBucket-${bucket.name}`}
>
<Grid item xs={12}>
<Grid container justifyContent={"space-between"}>
<Grid item xs={12} sm={7}>
@@ -232,7 +244,9 @@ const BucketListItem = ({
/>
</div>
)}
<h1 className={classes.bucketName}>{bucket.name}</h1>
<h1 className={classes.bucketName}>
{bucket.name} {manageAllowed}
</h1>
</Grid>
<Grid item xs={12}>
<Grid container className={classes.bucketInfo}>
@@ -251,43 +265,6 @@ const BucketListItem = ({
</Grid>
</Grid>
<Grid item xs={12} sm={5} className={classes.bucketActionButtons}>
{!noManage && (
<SecureComponent
scopes={IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN]}
resource={bucket.name}
>
<TooltipWrapper
tooltip={
manageAllowed
? "Manage Bucket"
: permissionTooltipHelper(
IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN],
"managing this bucket"
)
}
>
<Button
onClick={() => navigate(`/buckets/${bucket.name}/admin`)}
label={"Manage"}
icon={<SettingsIcon />}
color={"primary"}
variant={"regular"}
id={`manage-${bucket.name}`}
disabled={!manageAllowed}
/>
</TooltipWrapper>
</SecureComponent>
)}
<TooltipWrapper tooltip={"Browse"}>
<Button
onClick={() => navigate(`/buckets/${bucket.name}/browse`)}
label={"Browse"}
icon={<ArrowRightIcon />}
color={"primary"}
variant={"callAction"}
id={`browse-${bucket.name}`}
/>
</TooltipWrapper>
<Box display={{ xs: "none", sm: "block" }}>
<div style={{ marginBottom: 10 }} />
</Box>
@@ -330,4 +307,4 @@ const BucketListItem = ({
);
};
export default withStyles(styles)(BucketListItem);
export default BucketListItem;

View File

@@ -106,7 +106,7 @@ const CreatePathModal = ({
.filter((splitItem) => splitItem.trim() !== "")
.join("/");
const newPath = `/buckets/${bucketName}/browse/${encodeURLString(
const newPath = `/browser/${bucketName}/${encodeURLString(
`${folderPath}${cleanPathURL}/`
)}`;
navigate(newPath);

View File

@@ -283,11 +283,11 @@ const ListObjects = () => {
const [canPreviewFile, setCanPreviewFile] = useState<boolean>(false);
const [quota, setQuota] = useState<BucketQuota | null>(null);
const pathSegment = location.pathname.split("/browse/");
const internalPaths = pathSegment.length === 2 ? pathSegment[1] : "";
const bucketName = params.bucketName || "";
const pathSegment = location.pathname.split(`/browser/${bucketName}/`);
const internalPaths = pathSegment.length === 2 ? pathSegment[1] : "";
const pageTitle = decodeURLString(internalPaths);
const currentPath = pageTitle.split("/").filter((i: string) => i !== "");
@@ -729,7 +729,7 @@ const ListObjects = () => {
URLItem = `${splitURLS.join("/")}/`;
}
navigate(`/buckets/${bucketName}/browse/${encodeURLString(URLItem)}`);
navigate(`/browser/${bucketName}/${encodeURLString(URLItem)}`);
}
dispatch(setObjectDetailsView(false));

View File

@@ -27,6 +27,7 @@ import { AppState, useAppDispatch } from "../../../../../../store";
import { selFeatures } from "../../../../consoleSlice";
import { encodeURLString } from "../../../../../../common/utils";
import {
setIsOpeningOD,
setLoadingObjects,
setLoadingVersions,
setObjectDetailsView,
@@ -142,7 +143,7 @@ const ListObjectsTable = () => {
const openPath = (idElement: string) => {
dispatch(setSelectedObjects([]));
const newPath = `/buckets/${bucketName}/browse${
const newPath = `/browser/${bucketName}${
idElement ? `/${encodeURLString(idElement)}` : ``
}`;
navigate(newPath);
@@ -154,6 +155,7 @@ const ListObjectsTable = () => {
`${idElement ? `${encodeURLString(idElement)}` : ``}`
)
);
dispatch(setIsOpeningOD(true));
};
const tableActions: ItemActions[] = [
{

View File

@@ -45,6 +45,7 @@ export interface WebsocketResponse {
error?: string;
request_end?: boolean;
data?: ObjectResponse[];
prefix?: string;
}
export interface ObjectResponse {
name: string;

View File

@@ -229,7 +229,7 @@ export const permissionItems = (
// splitURL has more items than bucket name, we can continue validating
if (splitURLARN.length > 1) {
splitURLARN.every((currentElementInPath, index) => {
// It is a wildcard element. We can stor the verification as value should be included (?)
// It is a wildcard element. We can store the verification as value should be included (?)
if (currentElementInPath === "*") {
return false;
}

View File

@@ -108,6 +108,8 @@ const ObjectManager = React.lazy(
() => import("./Common/ObjectManager/ObjectManager")
);
const ObjectBrowser = React.lazy(() => import("./ObjectBrowser/ObjectBrowser"));
const Buckets = React.lazy(() => import("./Buckets/Buckets"));
const Policies = React.lazy(() => import("./Policies/Policies"));
@@ -272,6 +274,23 @@ const Console = ({ classes }: IConsoleProps) => {
});
const consoleAdminRoutes: IRouteRule[] = [
{
component: ObjectBrowser,
path: IAM_PAGES.OBJECT_BROWSER_VIEW,
forceDisplay: true,
customPermissionFnc: () => {
const path = window.location.pathname;
const resource = path.match(/browser\/(.*)\//);
return (
resource &&
resource.length > 0 &&
hasPermission(
resource[1],
IAM_PAGES_PERMISSIONS[IAM_PAGES.OBJECT_BROWSER_VIEW]
)
);
},
},
{
component: Buckets,
path: IAM_PAGES.BUCKETS,
@@ -304,22 +323,7 @@ const Console = ({ classes }: IConsoleProps) => {
);
},
},
{
component: Buckets,
path: IAM_PAGES.BUCKETS_BROWSE_VIEW,
customPermissionFnc: () => {
const path = window.location.pathname;
const resource = path.match(/buckets\/(.*)\/browse*/);
return (
resource &&
resource.length > 0 &&
hasPermission(
resource[1],
IAM_PAGES_PERMISSIONS[IAM_PAGES.BUCKETS_BROWSE_VIEW]
)
);
},
},
{
component: Watch,
path: IAM_PAGES.TOOLS_WATCH,

View File

@@ -95,7 +95,7 @@ const BrowserBreadcrumbs = ({
let breadcrumbsMap = splitPaths.map((objectItem: string, index: number) => {
const subSplit = `${splitPaths.slice(0, index + 1).join("/")}/`;
const route = `/buckets/${bucketName}/browse/${
const route = `/browser/${bucketName}/${
subSplit ? `${encodeURLString(subSplit)}` : ``
}`;
@@ -140,7 +140,7 @@ const BrowserBreadcrumbs = ({
const listBreadcrumbs: any[] = [
<Fragment key={`breadcrumbs-root-path`}>
<Link
to={`/buckets/${bucketName}/browse`}
to={`/browser/${bucketName}`}
onClick={() => {
dispatch(setVersionsModeEnabled({ status: false, objectName: "" }));
}}

View File

@@ -0,0 +1,312 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Theme } from "@mui/material/styles";
import { Button } from "mds";
import createStyles from "@mui/styles/createStyles";
import { LinearProgress } from "@mui/material";
import Grid from "@mui/material/Grid";
import { Bucket, BucketList } from "../Buckets/types";
import { BucketsIcon } from "../../../icons";
import {
actionsTray,
containerForHeader,
searchField,
} from "../Common/FormComponents/common/styleLibrary";
import { ErrorResponseHandler } from "../../../common/types";
import api from "../../../common/api";
import PageHeader from "../Common/PageHeader/PageHeader";
import HelpBox from "../../../common/HelpBox";
import RefreshIcon from "../../../icons/RefreshIcon";
import { SecureComponent } from "../../../common/SecureComponent";
import {
CONSOLE_UI_RESOURCE,
IAM_PAGES,
IAM_SCOPES,
permissionTooltipHelper,
} from "../../../common/SecureComponent/permissions";
import PageLayout from "../Common/Layout/PageLayout";
import SearchBox from "../Common/SearchBox";
import hasPermission from "../../../common/SecureComponent/accessControl";
import { setErrorSnackMessage } from "../../../systemSlice";
import { useAppDispatch } from "../../../store";
import { useSelector } from "react-redux";
import { selFeatures } from "../consoleSlice";
import AutoColorIcon from "../Common/Components/AutoColorIcon";
import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper";
import AButton from "../Common/AButton/AButton";
import { setLoadingObjects } from "../ObjectBrowser/objectBrowserSlice";
import makeStyles from "@mui/styles/makeStyles";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import { niceBytesInt } from "../../../common/utils";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
bulkSelect: {
marginLeft: 8,
"&:hover": {
backgroundColor: theme.palette.primary.main,
},
"&.MuiButton-contained": {
backgroundColor: theme.palette.primary.main,
},
},
bucketList: {
marginTop: 25,
height: "calc(100vh - 211px)",
"&.isEmbedded": {
height: "calc(100vh - 128px)",
},
},
searchField: {
...searchField.searchField,
minWidth: 380,
"@media (max-width: 900px)": {
minWidth: 220,
},
},
...actionsTray,
...containerForHeader(theme.spacing(4)),
})
);
const OBListBuckets = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const classes = useStyles();
const [records, setRecords] = useState<Bucket[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [filterBuckets, setFilterBuckets] = useState<string>("");
const features = useSelector(selFeatures);
const obOnly = !!features?.includes("object-browser-only");
useEffect(() => {
if (loading) {
const fetchRecords = () => {
setLoading(true);
api
.invoke("GET", `/api/v1/buckets`)
.then((res: BucketList) => {
setLoading(false);
setRecords(res.buckets || []);
dispatch(setLoadingObjects(true));
})
.catch((err: ErrorResponseHandler) => {
setLoading(false);
dispatch(setErrorSnackMessage(err));
});
};
fetchRecords();
}
}, [loading, dispatch]);
const filteredRecords = records.filter((b: Bucket) => {
if (filterBuckets === "") {
return true;
} else {
return b.name.indexOf(filterBuckets) >= 0;
}
});
const hasBuckets = records.length > 0;
const canListBuckets = hasPermission("*", [IAM_SCOPES.S3_LIST_BUCKET]);
const tableActions = [
{
type: "view",
onClick: (bucket: Bucket) => {
navigate(`${IAM_PAGES.OBJECT_BROWSER_VIEW}/${bucket.name}`);
},
},
];
return (
<Fragment>
{!obOnly && <PageHeader label={"Object Browser"} />}
<PageLayout>
<Grid item xs={12} className={classes.actionsTray} display="flex">
{obOnly && (
<Grid item xs>
<AutoColorIcon marginRight={15} marginTop={10} />
</Grid>
)}
{hasBuckets && (
<SearchBox
onChange={setFilterBuckets}
placeholder="Filter Buckets"
overrideClass={classes.searchField}
value={filterBuckets}
/>
)}
<Grid
item
xs={12}
display={"flex"}
alignItems={"center"}
justifyContent={"flex-end"}
sx={{
"& button": {
marginLeft: "8px",
},
}}
>
<TooltipWrapper tooltip={"Refresh"}>
<Button
id={"refresh-buckets"}
onClick={() => {
setLoading(true);
}}
icon={<RefreshIcon />}
variant={"regular"}
/>
</TooltipWrapper>
</Grid>
</Grid>
{loading && <LinearProgress />}
{!loading && (
<Grid
item
xs={12}
className={`${classes.bucketList} ${obOnly ? "isEmbedded" : ""}`}
>
{filteredRecords.length !== 0 && (
<TableWrapper
isLoading={loading}
records={filteredRecords}
entityName={"Buckets"}
idField={"name"}
columns={[
{
label: "Name",
elementKey: "name",
renderFunction: (label) => (
<span id={`browse-${label}`}>{label}</span>
),
},
{
label: "Objects",
elementKey: "objects",
renderFunction: (size: number) => size || 0,
},
{
label: "Size",
elementKey: "size",
renderFunction: (size: number) => niceBytesInt(size || 0),
},
{
label: "Access",
renderFullObject: true,
renderFunction: (bucket: Bucket) => {
let access = [];
if (bucket.rw_access?.read) {
access.push("R");
}
if (bucket.rw_access?.write) {
access.push("W");
}
return <span>{access.join("/")}</span>;
},
},
]}
itemActions={tableActions}
/>
)}
{filteredRecords.length === 0 && filterBuckets !== "" && (
<Grid
container
justifyContent={"center"}
alignContent={"center"}
alignItems={"center"}
>
<Grid item xs={8}>
<HelpBox
iconComponent={<BucketsIcon />}
title={"No Results"}
help={
<Fragment>
No buckets match the filtering condition
</Fragment>
}
/>
</Grid>
</Grid>
)}
{!hasBuckets && (
<Grid
container
justifyContent={"center"}
alignContent={"center"}
alignItems={"center"}
>
<Grid item xs={8}>
<HelpBox
iconComponent={<BucketsIcon />}
title={"Buckets"}
help={
<Fragment>
MinIO uses buckets to organize objects. A bucket is
similar to a folder or directory in a filesystem, where
each bucket can hold an arbitrary number of objects.
<br />
{canListBuckets ? (
""
) : (
<Fragment>
<br />
{permissionTooltipHelper(
[IAM_SCOPES.S3_LIST_BUCKET],
"view the buckets on this server"
)}
<br />
</Fragment>
)}
<SecureComponent
scopes={[IAM_SCOPES.S3_CREATE_BUCKET]}
resource={CONSOLE_UI_RESOURCE}
>
<br />
To get started,&nbsp;
<AButton
onClick={() => {
navigate(IAM_PAGES.ADD_BUCKETS);
}}
>
Create a Bucket.
</AButton>
</SecureComponent>
</Fragment>
}
/>
</Grid>
</Grid>
)}
</Grid>
)}
</PageLayout>
</Fragment>
);
};
export default OBListBuckets;

View File

@@ -0,0 +1,79 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Suspense } from "react";
import { Navigate, Route, Routes } from "react-router-dom";
import { IAM_PAGES } from "../../../common/SecureComponent/permissions";
import LoadingComponent from "../../../common/LoadingComponent";
import NotFoundPage from "../../NotFoundPage";
import OBBucketList from "./OBBucketList";
const BrowserHandler = React.lazy(
() => import("../Buckets/BucketDetails/BrowserHandler")
);
const AddBucket = React.lazy(
() => import("../Buckets/ListBuckets/AddBucket/AddBucket")
);
const ObjectBrowser = () => {
return (
<Routes>
<Route
path={IAM_PAGES.ADD_BUCKETS}
element={
<Suspense fallback={<LoadingComponent />}>
<AddBucket />
</Suspense>
}
/>
<Route
path="/"
element={
<Suspense fallback={<LoadingComponent />}>
<OBBucketList />
</Suspense>
}
/>
<Route
path="/:bucketName/*"
element={
<Suspense fallback={<LoadingComponent />}>
<BrowserHandler />
</Suspense>
}
/>
<Route
path=":bucketName/"
element={
<Suspense fallback={<LoadingComponent />}>
<BrowserHandler />
</Suspense>
}
/>
<Route element={<Navigate to={`/browser`} />} path="*" />
<Route
element={
<Suspense fallback={<LoadingComponent />}>
<NotFoundPage />
</Suspense>
}
/>
</Routes>
);
};
export default ObjectBrowser;

View File

@@ -63,6 +63,7 @@ const initialState: ObjectBrowserState = {
selectedPreview: null,
previewOpen: false,
shareFileModalOpen: false,
isOpeningObjectDetail: false,
};
export const objectBrowserSlice = createSlice({
@@ -336,6 +337,9 @@ export const objectBrowserSlice = createSlice({
);
}
},
setIsOpeningOD: (state, action: PayloadAction<boolean>) => {
state.isOpeningObjectDetail = action.payload;
},
},
});
export const {
@@ -378,6 +382,7 @@ export const {
setShareFileModalOpen,
setLoadingRecords,
restoreLocalObjectList,
setIsOpeningOD,
} = objectBrowserSlice.actions;
export default objectBrowserSlice.reducer;

View File

@@ -89,6 +89,7 @@ export interface ObjectBrowserState {
selectedPreview: BucketObjectItem | null;
previewOpen: boolean;
shareFileModalOpen: boolean;
isOpeningObjectDetail: boolean;
}
export interface ObjectManager {

View File

@@ -70,7 +70,7 @@ export const routesAsKbarActions = (
name: buck.name,
section: "List of Buckets",
perform: () => {
navigate(`/buckets/${buck.name}/browse`);
navigate(`/browser/${buck.name}`);
},
icon: <BucketsIcon />,
}),

View File

@@ -51,6 +51,7 @@ import {
DocumentationIcon,
LambdaIcon,
LicenseIcon,
ObjectBrowserIcon,
RecoverIcon,
StorageIcon,
TenantsOutlineIcon,
@@ -70,11 +71,11 @@ export const validRoutes = (
let consoleMenus: IMenuItem[] = [
{
group: "User",
name: "Buckets",
id: "buckets",
name: "Object Browser",
id: "object-browser",
component: NavLink,
to: IAM_PAGES.BUCKETS,
icon: BucketsMenuIcon,
to: IAM_PAGES.OBJECT_BROWSER_VIEW,
icon: ObjectBrowserIcon,
forceDisplay: true,
children: [],
},
@@ -108,7 +109,16 @@ export const validRoutes = (
);
},
},
{
group: "Administrator",
name: "Buckets",
id: "buckets",
component: NavLink,
to: IAM_PAGES.BUCKETS,
icon: BucketsMenuIcon,
forceDisplay: true,
children: [],
},
{
group: "Administrator",
name: "Identity",

View File

@@ -29,23 +29,6 @@ test("Buckets sidebar item exists", async (t) => {
await t.useRole(roles.bucketWrite).expect(bucketsExist).ok();
});
test
.before(async (t) => {
// Create a bucket
await functions.setUpBucket(t, "bucketwrite1");
})("Browse button exists", async (t) => {
const testBucketBrowseButton = testBucketBrowseButtonFor("bucketwrite1");
await t
.useRole(roles.bucketWrite)
.navigateTo("http://localhost:9090/buckets")
.expect(testBucketBrowseButton.exists)
.ok();
})
.after(async (t) => {
// Cleanup created bucket and corresponding uploads
await functions.cleanUpBucketAndUploads(t, "bucketwrite1");
});
test
.before(async (t) => {
// Create a bucket
@@ -78,7 +61,7 @@ test
const testBucketBrowseButton = testBucketBrowseButtonFor("bucketwrite2");
await t
.useRole(roles.bucketWrite)
.navigateTo("http://localhost:9090/buckets")
.navigateTo("http://localhost:9090/browser")
.click(testBucketBrowseButton)
.expect(uploadExists)
.ok();
@@ -96,7 +79,7 @@ test
const testBucketBrowseButton = testBucketBrowseButtonFor("bucketwrite3");
await t
.useRole(roles.bucketWrite)
.navigateTo("http://localhost:9090/buckets")
.navigateTo("http://localhost:9090/browser")
.click(testBucketBrowseButton)
// Upload object to bucket
.setFilesToUpload(elements.uploadInput, "../uploads/test.txt");
@@ -113,7 +96,7 @@ test
})("Object list table is disabled", async (t) => {
await t
.useRole(roles.bucketWrite)
.navigateTo("http://localhost:9090/buckets")
.navigateTo("http://localhost:9090/browser")
.click(testBucketBrowseButtonFor("bucketwrite4"))
.expect(elements.bucketsTableDisabled.exists)
.ok();

View File

@@ -43,7 +43,7 @@ test
})("Delete button is disabled for object inside bucket", async (t) => {
await t
.useRole(roles.deleteObjectWithPrefixOnly)
.navigateTo(`http://localhost:9090/buckets`)
.navigateTo(`http://localhost:9090/browser`)
.click(test1BucketBrowseButton)
.click(
Selector(".ReactVirtualized__Table__rowColumn").withText("test.txt")
@@ -69,7 +69,7 @@ test
async (t) => {
await t
.useRole(roles.deleteObjectWithPrefixOnly)
.navigateTo(`http://localhost:9090/buckets`)
.navigateTo(`http://localhost:9090/browser`)
.click(test2BucketBrowseButton)
.click(
Selector(".ReactVirtualized__Table__rowColumn").withText(
@@ -103,7 +103,7 @@ test
async (t) => {
await t
.useRole(roles.deleteObjectWithPrefixOnly)
.navigateTo(`http://localhost:9090/buckets`)
.navigateTo(`http://localhost:9090/browser`)
.click(test3BucketBrowseButton)
.click(
Selector(".ReactVirtualized__Table__rowColumn").withText(

View File

@@ -33,12 +33,12 @@ test
await functions.setVersioned(t, "bucketdelete3");
await t
.useRole(roles.bucketReadWrite)
.navigateTo("http://localhost:9090/buckets")
.navigateTo("http://localhost:9090/browser")
.click(testBucketBrowseButtonFor("bucketdelete3"))
// Upload object to bucket
.setFilesToUpload(elements.uploadInput, "../uploads/test.txt")
.wait(1000)
.navigateTo("http://localhost:9090/buckets")
.navigateTo("http://localhost:9090/browser")
.click(testBucketBrowseButtonFor("bucketdelete3"))
// Upload object to bucket
.setFilesToUpload(elements.uploadInput, "../uploads/test.txt")
@@ -46,7 +46,7 @@ test
})("All versions of an object can be deleted from a bucket", async (t) => {
await t
.useRole(roles.bucketReadWrite)
.navigateTo("http://localhost:9090/buckets")
.navigateTo("http://localhost:9090/browser")
.click(testBucketBrowseButtonFor("bucketdelete3"))
.click(
"div.ReactVirtualized__Grid.ReactVirtualized__Table__Grid > div > div:nth-child(1)"

View File

@@ -31,7 +31,7 @@ test
await functions.setVersioned(t, "bucketobjecttags");
await t
.useRole(roles.bucketObjectTags)
.navigateTo("http://localhost:9090/buckets")
.navigateTo("http://localhost:9090/browser")
.click(testBucketBrowseButtonFor("bucketobjecttags"))
// Upload object to bucket
.setFilesToUpload(elements.uploadInput, "../uploads/test.txt")
@@ -39,7 +39,7 @@ test
})("Tags can be created and deleted", async (t) => {
await t
.useRole(roles.bucketObjectTags)
.navigateTo("http://localhost:9090/buckets")
.navigateTo("http://localhost:9090/browser")
.click(testBucketBrowseButtonFor("bucketobjecttags"))
.click(
"div.ReactVirtualized__Grid.ReactVirtualized__Table__Grid > div > div:nth-child(1)"
@@ -69,7 +69,7 @@ test
await functions.setVersioned(t, "bucketcannottag");
await t
.useRole(roles.bucketCannotTag)
.navigateTo("http://localhost:9090/buckets")
.navigateTo("http://localhost:9090/browser")
.click(testBucketBrowseButtonFor("bucketcannottag"))
// Upload object to bucket
.setFilesToUpload(elements.uploadInput, "../uploads/test.txt")
@@ -77,7 +77,7 @@ test
})("User should not be able to create tag", async (t) => {
await t
.useRole(roles.bucketCannotTag)
.navigateTo("http://localhost:9090/buckets")
.navigateTo("http://localhost:9090/browser")
.click(testBucketBrowseButtonFor("bucketcannottag"))
.click(
"div.ReactVirtualized__Grid.ReactVirtualized__Table__Grid > div > div:nth-child(1)"

View File

@@ -41,7 +41,7 @@ test
await new Promise((resolve) => setTimeout(resolve, 2000));
await t
.useRole(roles.bucketRead)
.navigateTo("http://localhost:9090/buckets")
.navigateTo("http://localhost:9090/browser")
.expect(testBucketBrowseButtonFor("bucketread1").exists)
.ok();
})
@@ -79,7 +79,7 @@ test
await functions.setUpBucket(t, "aread3");
await t
.useRole(roles.admin)
.navigateTo("http://localhost:9090/buckets")
.navigateTo("http://localhost:9090/browser")
.click(testBucketBrowseButtonFor("aread3"))
// Upload object to bucket
.setFilesToUpload(elements.uploadInput, "../uploads/test.txt")
@@ -88,7 +88,7 @@ test
await new Promise((resolve) => setTimeout(resolve, 2000));
await t
.useRole(roles.bucketRead)
.navigateTo("http://localhost:9090/buckets")
.navigateTo("http://localhost:9090/browser")
.wait(2000)
.click(testBucketBrowseButtonFor("aread3"))
.wait(2000)

View File

@@ -117,7 +117,7 @@ test
await new Promise((resolve) => setTimeout(resolve, 2000));
await t
.useRole(roles.bucketRead)
.navigateTo("http://localhost:9090/buckets")
.navigateTo("http://localhost:9090/browser")
.expect(
namedTestBucketBrowseButtonFor(`${TEST_BUCKET_NAME_SPECIFIC}-4`).exists
)
@@ -157,7 +157,7 @@ test
await functions.setUpNamedBucket(t, `${TEST_BUCKET_NAME_SPECIFIC}-6`);
await t
.useRole(roles.admin)
.navigateTo("http://localhost:9090/buckets")
.navigateTo("http://localhost:9090/browser")
.click(namedTestBucketBrowseButtonFor(`${TEST_BUCKET_NAME_SPECIFIC}-6`))
// Upload object to bucket
.setFilesToUpload(elements.uploadInput, "../uploads/test.txt")
@@ -166,7 +166,7 @@ test
await new Promise((resolve) => setTimeout(resolve, 2000));
await t
.useRole(roles.bucketRead)
.navigateTo("http://localhost:9090/buckets")
.navigateTo("http://localhost:9090/browser")
.click(namedTestBucketBrowseButtonFor(`${TEST_BUCKET_NAME_SPECIFIC}-6`))
.expect(elements.table.exists)
.ok();
@@ -191,7 +191,7 @@ test
);
await t
.useRole(roles.bucketSpecific)
.navigateTo("http://localhost:9090/buckets")
.navigateTo("http://localhost:9090/browser")
.expect(testBucketBrowseButton.exists)
.ok();
})
@@ -240,7 +240,7 @@ test
);
await t
.useRole(roles.bucketSpecific)
.navigateTo("http://localhost:9090/buckets")
.navigateTo("http://localhost:9090/browser")
.click(testBucketBrowseButton)
.expect(uploadExists)
.ok();
@@ -263,7 +263,7 @@ test
);
await t
.useRole(roles.bucketSpecific)
.navigateTo("http://localhost:9090/buckets")
.navigateTo("http://localhost:9090/browser")
.click(testBucketBrowseButton)
// Upload object to bucket
.setFilesToUpload(elements.uploadInput, "../uploads/test.txt");
@@ -283,7 +283,7 @@ test
})("Object list table is disabled", async (t) => {
await t
.useRole(roles.bucketSpecific)
.navigateTo("http://localhost:9090/buckets")
.navigateTo("http://localhost:9090/browser")
.click(namedTestBucketBrowseButtonFor(`${TEST_BUCKET_NAME_SPECIFIC}-11`))
.expect(elements.bucketsTableDisabled.exists)
.ok();

View File

@@ -61,7 +61,7 @@ test
async (t) => {
await t
.useRole(roles.conditions2)
.navigateTo(`http://localhost:9090/buckets`)
.navigateTo(`http://localhost:9090/browser`)
.click(test1BucketBrowseButton)
.click(
Selector(".ReactVirtualized__Table__rowColumn").withText("firstlevel")
@@ -111,7 +111,7 @@ test
})("User can browse from first level as policy has wildcard", async (t) => {
await t
.useRole(roles.conditions1)
.navigateTo(`http://localhost:9090/buckets`)
.navigateTo(`http://localhost:9090/browser`)
.click(test1BucketBrowseButton)
.click(
Selector(".ReactVirtualized__Table__rowColumn").withText("firstlevel")

View File

@@ -28,12 +28,12 @@ test
await functions.setVersioned(t, "abucketrewind");
await t
.useRole(roles.bucketReadWrite)
.navigateTo("http://localhost:9090/buckets")
.navigateTo("http://localhost:9090/browser")
.click(testBucketBrowseButtonFor("abucketrewind"))
// Upload object to bucket
.setFilesToUpload(elements.uploadInput, "../uploads/test.txt")
.wait(1000)
.navigateTo("http://localhost:9090/buckets")
.navigateTo("http://localhost:9090/browser")
.click(testBucketBrowseButtonFor("abucketrewind"))
// Upload object to bucket
.setFilesToUpload(elements.uploadInput, "../uploads/test.txt")
@@ -41,7 +41,7 @@ test
})("Rewind works in bucket", async (t) => {
await t
.useRole(roles.bucketReadWrite)
.navigateTo("http://localhost:9090/buckets")
.navigateTo("http://localhost:9090/browser")
.click(testBucketBrowseButtonFor("abucketrewind"))
.expect(elements.table.exists)
.ok()

View File

@@ -29,7 +29,7 @@ test
const uploadButton = elements.uploadButton;
await t
.useRole(roles.bucketWritePrefixOnly)
.navigateTo("http://localhost:9090/buckets/testcafe/browse")
.navigateTo("http://localhost:9090/browser/testcafe")
.click(uploadButton)
.expect(Selector("li").withText("Upload File").hasClass("Mui-disabled"))
.ok()
@@ -48,7 +48,7 @@ test
const uploadButton = elements.uploadButton;
await t
.useRole(roles.bucketWritePrefixOnly)
.navigateTo("http://localhost:9090/buckets/testcafe/browse/d3JpdGU=")
.navigateTo("http://localhost:9090/browser/testcafe/d3JpdGU=")
.click(uploadButton)
.expect(Selector("li").withText("Upload File").hasClass("Mui-disabled"))
.notOk()

View File

@@ -94,11 +94,7 @@ export const setVersionedBucket = (t, name) => {
};
export const namedManageButtonFor = (name) => {
return Selector("h1")
.withText(name)
.parent(4)
.find("button:enabled")
.withText("Manage");
return Selector("div").withAttribute("id", `manageBucket-${name}`);
};
export const manageButtonFor = (modifier) => {
@@ -122,9 +118,7 @@ export const cleanUpBucket = (t, modifier) => {
};
export const namedTestBucketBrowseButtonFor = (name) => {
return Selector("button:enabled")
.withAttribute("id", `browse-${name}`)
.withText("Browse");
return Selector("span").withAttribute("id", `browse-${name}`);
};
export const testBucketBrowseButtonFor = (modifier) => {

View File

@@ -2308,11 +2308,6 @@
dependencies:
"@types/unist" "*"
"@types/history@^4.7.11":
version "4.7.11"
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64"
integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==
"@types/hoist-non-react-statics@*", "@types/hoist-non-react-statics@^3.3.0", "@types/hoist-non-react-statics@^3.3.1":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
@@ -2487,23 +2482,6 @@
hoist-non-react-statics "^3.3.0"
redux "^4.0.0"
"@types/react-router-dom@^5.3.3":
version "5.3.3"
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83"
integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==
dependencies:
"@types/history" "^4.7.11"
"@types/react" "*"
"@types/react-router" "*"
"@types/react-router@*":
version "5.1.18"
resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.18.tgz#c8851884b60bc23733500d86c1266e1cfbbd9ef3"
integrity sha512-YYknwy0D0iOwKQgz9v8nOzt2J6l4gouBmDnWqUUznltOTaon+r8US8ky8HvN0tXvc38U9m6z/t2RsVsnd1zM0g==
dependencies:
"@types/history" "^4.7.11"
"@types/react" "*"
"@types/react-transition-group@^4.4.5":
version "4.4.5"
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.5.tgz#aae20dcf773c5aa275d5b9f7cdbca638abc5e416"
@@ -5929,10 +5907,10 @@ forwarded@0.2.0:
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
fp-ts@^2.12.1:
version "2.12.2"
resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-2.12.2.tgz#a191db2dbbb04f48a0e75050b94f57cc876c7b40"
integrity sha512-v8J7ud+nTkP5Zz17GhpCsY19wiRbB9miuj61nBcCJyDpu52zs9Z4O7OLDfYoKFQMJ9EsSZA7W1vRgC1d3jy5qw==
fp-ts@^2.9.5:
version "2.13.1"
resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-2.13.1.tgz#1bf2b24136cca154846af16752dc29e8fa506f2a"
integrity sha512-0eu5ULPS2c/jsa1lGFneEFFEdTbembJv8e4QKXeVJ3lm/5hyve06dlKZrpxmMwJt6rYen7sxmHHK2CLaXvWuWQ==
fraction.js@^4.2.0:
version "4.2.0"
@@ -6507,6 +6485,11 @@ http-proxy@^1.18.1:
follow-redirects "^1.0.0"
requires-port "^1.0.0"
http-status-codes@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-2.2.0.tgz#bb2efe63d941dfc2be18e15f703da525169622be"
integrity sha512-feERVo9iWxvnejp3SEfm/+oNG517npqL2/PIA8ORjyOZjGC7TwCRQsZylciLS64i6pJ0wRYz3rkXLRwbtFa8Ng==
https-proxy-agent@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
@@ -6911,6 +6894,11 @@ is-plain-obj@^4.0.0:
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0"
integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==
is-podman@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-podman/-/is-podman-1.0.1.tgz#284a7ba1e6987fff8af5d71d97a29d2a85b7996a"
integrity sha512-+5vbtF5FIg262iUa7gOIseIWTx0740RHiax7oSmJMhbfSoBIMQ/IacKKgfnGj65JGeH9lGEVQcdkDwhn1Em1mQ==
is-potential-custom-element-name@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
@@ -11108,10 +11096,10 @@ testcafe-browser-tools@2.0.23:
read-file-relative "^1.2.0"
which-promise "^1.0.0"
testcafe-hammerhead@24.7.2:
version "24.7.2"
resolved "https://registry.yarnpkg.com/testcafe-hammerhead/-/testcafe-hammerhead-24.7.2.tgz#e3665ead7b1df63593fc278f9345f8fbec815147"
integrity sha512-f9f/CuOtaeIq+avD8hFO6aMGCdc446R1+7h3TR+4vuqkOQaqow5SuwEI/QdCF05z8nENDcXGUORXx0hOPrGhlw==
testcafe-hammerhead@28.1.0:
version "28.1.0"
resolved "https://registry.yarnpkg.com/testcafe-hammerhead/-/testcafe-hammerhead-28.1.0.tgz#fcfc620730caaa42514854256a19b207059fc475"
integrity sha512-6J+U1MEV8L7OI467tkpOpewqYE6u0bhDTGQhK1s8cvDZtcvzm1ArFTRJvyG1wMb3/QaGrzoCHpEMRJ2BIK/0lw==
dependencies:
acorn-hammerhead "0.6.1"
asar "^2.0.1"
@@ -11168,10 +11156,10 @@ testcafe-hammerhead@>=19.4.0:
tunnel-agent "0.6.0"
webauth "^1.1.0"
testcafe-legacy-api@5.1.4:
version "5.1.4"
resolved "https://registry.yarnpkg.com/testcafe-legacy-api/-/testcafe-legacy-api-5.1.4.tgz#de913a79869abf9c5ff117eeb9adbef78519f4ff"
integrity sha512-CWjwGlRZdSuoWDIRBHKetpmDffR+/LKS6+69n8VM4mkLKgUwsP8p3MERHdx0obBn8wZ0LSyrYj8SCtv5f7oWZg==
testcafe-legacy-api@5.1.6:
version "5.1.6"
resolved "https://registry.yarnpkg.com/testcafe-legacy-api/-/testcafe-legacy-api-5.1.6.tgz#157b29902153cc086649f91960a4e45694481f50"
integrity sha512-Q451IdSUX1NmRfE8kzIcEeoqbUlLaMv2fwVNgQOBEFmA5E57c3jsIpLDTDqv6FPcNwdNMYIZMiB6tzlXB5wf1g==
dependencies:
async "3.2.3"
dedent "^0.6.0"
@@ -11188,13 +11176,13 @@ testcafe-legacy-api@5.1.4:
strip-bom "^2.0.0"
testcafe-hammerhead ">=19.4.0"
testcafe-reporter-dashboard@1.0.0-rc.3:
version "1.0.0-rc.3"
resolved "https://registry.yarnpkg.com/testcafe-reporter-dashboard/-/testcafe-reporter-dashboard-1.0.0-rc.3.tgz#9065f7265ea2d63c171f867f23b6d8d9e5d9f1ae"
integrity sha512-F4gphX9/KlZzEz26I9LwUw7DKdKFQEpsU4Pr04ssBOJCyZh4ST7kuOyB+JMqr4iJfE9zu6+g6aaXumM+ad61JA==
testcafe-reporter-dashboard@^0.2.7:
version "0.2.8"
resolved "https://registry.yarnpkg.com/testcafe-reporter-dashboard/-/testcafe-reporter-dashboard-0.2.8.tgz#66d14a9cffc7d98a56ea59f22e69aa01f6c18ac7"
integrity sha512-C/uTJoLgCWkBRlO6jaHDmHC4qAncZut/05s+gxPiFsPNKMZnbgAFEfzJHWOgt6B4eWbREruvuyH99Lr5xXo4wQ==
dependencies:
es6-promise "^4.2.8"
fp-ts "^2.12.1"
fp-ts "^2.9.5"
io-ts "^2.2.14"
io-ts-types "^0.5.15"
isomorphic-fetch "^3.0.0"
@@ -11202,7 +11190,7 @@ testcafe-reporter-dashboard@1.0.0-rc.3:
monocle-ts "^2.3.5"
newtype-ts "^0.3.4"
semver "^5.6.0"
uuid "3.3.3"
uuid "^9.0.0"
testcafe-reporter-json@^2.1.0:
version "2.2.0"
@@ -11234,10 +11222,10 @@ testcafe-safe-storage@^1.1.1:
resolved "https://registry.yarnpkg.com/testcafe-safe-storage/-/testcafe-safe-storage-1.1.2.tgz#dacfda9a51c77f61f11b13506d4004dd7f27eb73"
integrity sha512-6km7D26+KCQGeFlSQ9fVEU7tD8qdioSpqzxU8CCZkd2KzBS0jTFkqaJ54rPaLdd5+wdhgO3+as3LMm4F0EDeYA==
testcafe@^1.18.6:
version "1.20.1"
resolved "https://registry.yarnpkg.com/testcafe/-/testcafe-1.20.1.tgz#872265fb0dc943bd15da7f4cfaa2162df61e36e3"
integrity sha512-D5UQsR10zsqPSqN4uWxS8CjmpaRUSduo3hjQscIzww8/uZ3AmOYJOrIk+punpieCmcwclfGz6ic2pdRv21rNnA==
testcafe@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/testcafe/-/testcafe-2.1.0.tgz#382f1e8d62072ee02caf619bb43492d22d5d5a99"
integrity sha512-mfF9UX2U9RyjNfuviaOf/MYEQA7uGxe0n+VXJzdPMqpPjvILe/LrAopyxjkmW5MllZAGW0/tmwOy9xRZl50l7A==
dependencies:
"@babel/core" "^7.12.1"
"@babel/plugin-proposal-async-generator-functions" "^7.12.1"
@@ -11284,12 +11272,14 @@ testcafe@^1.18.6:
globby "^11.0.4"
graceful-fs "^4.1.11"
graphlib "^2.1.5"
http-status-codes "^2.2.0"
humanize-duration "^3.25.0"
import-lazy "^3.1.0"
indent-string "^1.2.2"
is-ci "^1.0.10"
is-docker "^2.0.0"
is-glob "^2.0.1"
is-podman "^1.0.1"
is-stream "^2.0.0"
json5 "^2.1.0"
lodash "^4.17.13"
@@ -11318,9 +11308,9 @@ testcafe@^1.18.6:
source-map-support "^0.5.16"
strip-bom "^2.0.0"
testcafe-browser-tools "2.0.23"
testcafe-hammerhead "24.7.2"
testcafe-legacy-api "5.1.4"
testcafe-reporter-dashboard "1.0.0-rc.3"
testcafe-hammerhead "28.1.0"
testcafe-legacy-api "5.1.6"
testcafe-reporter-dashboard "^0.2.7"
testcafe-reporter-json "^2.1.0"
testcafe-reporter-list "^2.1.0"
testcafe-reporter-minimal "^2.1.0"
@@ -11330,7 +11320,7 @@ testcafe@^1.18.6:
time-limit-promise "^1.0.2"
tmp "0.0.28"
tree-kill "^1.2.2"
typescript "^3.3.3"
typescript "4.7.4"
unquote "^1.1.1"
text-segmentation@^1.0.3:
@@ -11585,10 +11575,10 @@ typedarray-to-buffer@^3.1.5:
dependencies:
is-typedarray "^1.0.0"
typescript@^3.3.3:
version "3.9.10"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8"
integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==
typescript@4.7.4:
version "4.7.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
typescript@^4.4.3:
version "4.8.2"
@@ -11805,16 +11795,16 @@ utrie@^1.0.2:
dependencies:
base64-arraybuffer "^1.0.2"
uuid@3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866"
integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==
uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
uuid@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==
v8-to-istanbul@^8.1.0:
version "8.1.1"
resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz#77b752fd3975e31bbcef938f85e9bd1c7a8d60ed"

View File

@@ -43,6 +43,7 @@ type WSResponse struct {
RequestID int64 `json:"request_id,nonempty"`
Error string `json:"error,omitempty"`
RequestEnd bool `json:"request_end,omitempty"`
Prefix string `json:"prefix,omitempty"`
Data []ObjectResponse `json:"data,omitempty"`
}

View File

@@ -617,6 +617,7 @@ func (wsc *wsMinioClient) objectManager(session *models.Principal) {
writeChannel <- WSResponse{
RequestID: messageRequest.RequestID,
Error: lsObj.Err.Error(),
Prefix: messageRequest.Prefix,
}
continue
@@ -689,6 +690,7 @@ func (wsc *wsMinioClient) objectManager(session *models.Principal) {
writeChannel <- WSResponse{
RequestID: messageRequest.RequestID,
Error: lsObj.Err.String(),
Prefix: messageRequest.Prefix,
}
continue