Object Browser Refactor (#3066)

- Refactored navigation to be handled only with URL
- Refactored & simplified websocket
- Updated components to use mds
- Fixed an issue with Anonymous access and file selection
- Fixed an issue with anonymous access and download selection click
- Fixed an issue with object details selection on root path from a bucket
- Simplified reducer

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2023-10-02 18:02:03 -05:00
committed by GitHub
parent 078ce0e546
commit 083314ee2d
16 changed files with 552 additions and 670 deletions

View File

@@ -17,121 +17,26 @@
import React, { Fragment, useCallback, useEffect } from "react";
import { useSelector } from "react-redux";
import { useLocation, useParams } from "react-router-dom";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { api } from "api";
import { AppState, useAppDispatch } from "../../../../store";
import { containerForHeader } from "../../Common/FormComponents/common/styleLibrary";
import ListObjects from "../ListBuckets/Objects/ListObjects/ListObjects";
import { IAM_SCOPES } from "../../../../common/SecureComponent/permissions";
import { decodeURLString, encodeURLString } from "../../../../common/utils";
import {
errorInConnection,
newMessage,
resetMessages,
setIsOpeningOD,
setIsVersioned,
setLoadingLocking,
setLoadingObjectInfo,
setLoadingObjects,
setLoadingRecords,
setLoadingVersioning,
setLoadingVersions,
setLockingEnabled,
setObjectDetailsView,
setRecords,
setRequestInProgress,
setSelectedObjectView,
setSimplePathHandler,
setVersionsModeEnabled,
} from "../../ObjectBrowser/objectBrowserSlice";
import ListObjects from "../ListBuckets/Objects/ListObjects/ListObjects";
import hasPermission from "../../../../common/SecureComponent/accessControl";
import { IMessageEvent } from "websocket";
import { wsProtocol } from "../../../../utils/wsUtils";
import {
WebsocketRequest,
WebsocketResponse,
} from "../ListBuckets/Objects/ListObjects/types";
import { decodeURLString, encodeURLString } from "../../../../common/utils";
import { permissionItems } from "../ListBuckets/Objects/utils";
import { setErrorSnackMessage } from "../../../../systemSlice";
import OBHeader from "../../ObjectBrowser/OBHeader";
import { api } from "api";
const styles = (theme: Theme) =>
createStyles({
...containerForHeader,
});
let objectsWS: WebSocket;
let currentRequestID: number = 0;
let errorCounter: number = 0;
let wsInFlight: boolean = false;
const initWSConnection = (
openCallback?: () => void,
onMessageCallback?: (message: IMessageEvent) => void,
connErrorCallback?: (message: string) => void,
) => {
if (wsInFlight) {
return;
}
wsInFlight = true;
const url = new URL(window.location.toString());
const isDev = process.env.NODE_ENV === "development";
const port = isDev ? "9090" : url.port;
// check if we are using base path, if not this always is `/`
const baseLocation = new URL(document.baseURI);
const baseUrl = baseLocation.pathname;
const wsProt = wsProtocol(url.protocol);
objectsWS = new WebSocket(
`${wsProt}://${url.hostname}:${port}${baseUrl}ws/objectManager`,
);
objectsWS.onopen = () => {
wsInFlight = false;
if (openCallback) {
openCallback();
}
errorCounter = 0;
};
if (onMessageCallback) {
objectsWS.onmessage = onMessageCallback;
}
const reconnectFn = () => {
if (errorCounter <= 5) {
initWSConnection(() => {}, onMessageCallback, connErrorCallback);
errorCounter += 1;
} else {
console.error(
"Websocket not available. Please review that your environment settings are enabled to allow websocket connections and that requests are made from the same origin.",
);
if (connErrorCallback) {
connErrorCallback(
"Couldn't establish WebSocket connection. Please review your configuration and try again.",
);
}
}
};
objectsWS.onclose = () => {
wsInFlight = false;
console.warn("Websocket Disconnected. Attempting Reconnection...");
// We reconnect after 3 seconds
setTimeout(reconnectFn, 3000);
};
objectsWS.onerror = () => {
wsInFlight = false;
console.error("Error in websocket connection. Attempting reconnection...");
// Onclose will be triggered by specification, reconnect function will be executed there to avoid duplicated requests
};
};
const BrowserHandler = () => {
const dispatch = useAppDispatch();
@@ -151,159 +56,100 @@ const BrowserHandler = () => {
const showDeleted = useSelector(
(state: AppState) => state.objectBrowser.showDeleted,
);
const allowResources = useSelector(
(state: AppState) => state.console.session.allowResources,
);
const loadingObjects = useSelector(
(state: AppState) => state.objectBrowser.loadingObjects,
const requestInProgress = useSelector(
(state: AppState) => state.objectBrowser.requestInProgress,
);
const loadingLocking = useSelector(
(state: AppState) => state.objectBrowser.loadingLocking,
);
const loadRecords = useSelector(
(state: AppState) => state.objectBrowser.loadRecords,
);
const selectedInternalPaths = useSelector(
(state: AppState) => state.objectBrowser.selectedInternalPaths,
const reloadObjectsList = useSelector(
(state: AppState) => state.objectBrowser.reloadObjectsList,
);
const simplePath = useSelector(
(state: AppState) => state.objectBrowser.simplePath,
);
const isOpeningOD = useSelector(
(state: AppState) => state.objectBrowser.isOpeningObjectDetail,
);
const anonymousMode = useSelector(
(state: AppState) => state.system.anonymousMode,
);
const selectedBucket = useSelector(
(state: AppState) => state.objectBrowser.selectedBucket,
);
const records = useSelector((state: AppState) => state.objectBrowser.records);
const bucketName = params.bucketName || "";
const pathSegment = location.pathname.split(`/browser/${bucketName}/`);
const internalPaths = pathSegment.length === 2 ? pathSegment[1] : "";
/*WS Request Handlers*/
const onMessageCallBack = useCallback(
(message: IMessageEvent) => {
// reset start status
dispatch(setLoadingObjects(false));
const initWSRequest = useCallback(
(path: string) => {
let currDate = new Date();
const response: WebsocketResponse = JSON.parse(message.data.toString());
if (currentRequestID === response.request_id) {
// If response is not from current request, we can omit
if (response.request_id !== currentRequestID) {
return;
}
let date = currDate.toISOString();
if (
response.error ===
"The Access Key Id you provided does not exist in our records."
) {
// Session expired.
window.location.reload();
} else if (response.error === "Access Denied.") {
const internalPathsPrefix = response.prefix;
let pathPrefix = "";
if (internalPathsPrefix) {
const decodedPath = decodeURLString(internalPathsPrefix);
pathPrefix = decodedPath.endsWith("/")
? decodedPath
: decodedPath + "/";
}
const permitItems = permissionItems(
response.bucketName || bucketName,
pathPrefix,
allowResources || [],
);
if (!permitItems || permitItems.length === 0) {
dispatch(
setErrorSnackMessage({
errorMessage: response.error,
detailedError: response.error,
}),
);
} else {
dispatch(setRecords(permitItems));
}
return;
}
// This indicates final messages is received.
if (response.request_end) {
dispatch(setLoadingObjects(false));
dispatch(setLoadingRecords(false));
return;
}
if (response.data) {
dispatch(newMessage(response.data));
}
if (rewindDate !== null && rewindEnabled) {
date = rewindDate;
}
const payloadData = {
bucketName,
path,
rewindMode: rewindEnabled || showDeleted,
date: date,
};
dispatch({ type: "socket/OBRequest", payload: payloadData });
},
[dispatch, allowResources, bucketName],
[bucketName, showDeleted, rewindDate, rewindEnabled, dispatch],
);
const initWSRequest = useCallback(
(path: string, date: Date) => {
if (objectsWS && objectsWS.readyState === 1) {
try {
const newRequestID = currentRequestID + 1;
dispatch(resetMessages());
dispatch(errorInConnection(false));
// Common path load
const pathLoad = useCallback(
(forceLoad: boolean = false) => {
const decodedInternalPaths = decodeURLString(internalPaths);
const request: WebsocketRequest = {
bucket_name: bucketName,
prefix: encodeURLString(path),
mode: rewindEnabled || showDeleted ? "rewind" : "objects",
date: date.toISOString(),
request_id: newRequestID,
};
// We exit Versions mode in case of path change
dispatch(setVersionsModeEnabled({ status: false }));
objectsWS.send(JSON.stringify(request));
let searchPath = decodedInternalPaths;
// We store the new ID for the requestID
currentRequestID = newRequestID;
} catch (e) {
console.error(e);
}
} else {
// Socket is disconnected, we request reconnection but will need to recreate call
const dupRequest = () => {
initWSRequest(path, date);
};
if (!decodedInternalPaths.endsWith("/") && decodedInternalPaths !== "") {
searchPath = `${decodedInternalPaths
.split("/")
.slice(0, -1)
.join("/")}/`;
}
const fatalWSError = (message: string) => {
dispatch(
setErrorSnackMessage({
errorMessage: message,
detailedError: message,
}),
);
dispatch(errorInConnection(true));
};
if (searchPath === "/") {
searchPath = "";
}
initWSConnection(dupRequest, onMessageCallBack, fatalWSError);
// If the path is different of the actual path or reload objects list is requested, then we initialize a new request to load a new record set.
if (
searchPath !== simplePath ||
bucketName !== selectedBucket ||
forceLoad
) {
dispatch(setRequestInProgress(true));
initWSRequest(searchPath);
}
},
[bucketName, rewindEnabled, showDeleted, dispatch, onMessageCallBack],
[
internalPaths,
dispatch,
simplePath,
selectedBucket,
bucketName,
initWSRequest,
],
);
useEffect(() => {
return () => {
const request: WebsocketRequest = {
mode: "cancel",
request_id: currentRequestID,
};
if (objectsWS && objectsWS.readyState === 1) {
objectsWS.send(JSON.stringify(request));
}
dispatch({ type: "socket/OBCancelLast" });
};
}, []);
}, [dispatch]);
// Object Details handler
useEffect(() => {
const decodedIPaths = decodeURLString(internalPaths);
@@ -312,9 +158,6 @@ const BrowserHandler = () => {
if (decodedIPaths.endsWith("/") || decodedIPaths === "") {
dispatch(setObjectDetailsView(false));
dispatch(setSelectedObjectView(null));
dispatch(
setSimplePathHandler(decodedIPaths === "" ? "/" : decodedIPaths),
);
dispatch(setLoadingLocking(true));
} else {
dispatch(setLoadingObjectInfo(true));
@@ -325,45 +168,20 @@ const BrowserHandler = () => {
`${decodedIPaths ? `${encodeURLString(decodedIPaths)}` : ``}`,
),
);
dispatch(
setSimplePathHandler(
`${decodedIPaths.split("/").slice(0, -1).join("/")}/`,
),
);
}
}, [bucketName, internalPaths, rewindDate, rewindEnabled, dispatch]);
// Direct file access effect / prefix
// Navigation Listing Request
useEffect(() => {
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);
pathLoad(false);
}, [pathLoad]);
if (
!decodedInternalPaths.endsWith("/") &&
simplePath !== decodedInternalPaths &&
decodedInternalPaths !== ""
) {
setLoadingRecords(true);
const parentPath = `${decodedInternalPaths
.split("/")
.slice(0, -1)
.join("/")}/`;
initWSRequest(parentPath, new Date());
}
// Reload Handler
useEffect(() => {
if (reloadObjectsList && records.length === 0 && !requestInProgress) {
pathLoad(true);
}
dispatch(setIsOpeningOD(false));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
loadingObjects,
loadRecords,
dispatch,
internalPaths,
initWSRequest,
rewindEnabled,
simplePath,
]);
}, [reloadObjectsList, records, requestInProgress, pathLoad]);
const displayListObjects =
hasPermission(bucketName, [
@@ -371,51 +189,6 @@ const BrowserHandler = () => {
IAM_SCOPES.S3_ALL_LIST_BUCKET,
]) || anonymousMode;
// Common objects list
useEffect(() => {
// begin watch if bucketName in bucketList and start pressed
if (loadingObjects && displayListObjects) {
let pathPrefix = "";
if (internalPaths) {
const decodedPath = decodeURLString(internalPaths);
// internalPaths are selected (file details), we split and get parent folder
if (selectedInternalPaths === internalPaths) {
pathPrefix = `${decodeURLString(internalPaths)
.split("/")
.slice(0, -1)
.join("/")}/`;
} else {
pathPrefix = decodedPath.endsWith("/")
? decodedPath
: decodedPath + "/";
}
}
let requestDate = new Date();
if (rewindEnabled && rewindDate) {
requestDate = new Date(rewindDate);
}
initWSRequest(pathPrefix, requestDate);
} else {
dispatch(setLoadingObjects(false));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
loadingObjects,
internalPaths,
dispatch,
rewindDate,
rewindEnabled,
displayListObjects,
initWSRequest,
]);
useEffect(() => {
dispatch(setVersionsModeEnabled({ status: false }));
}, [internalPaths, dispatch]);
useEffect(() => {
if (loadingVersioning && !anonymousMode) {
if (displayListObjects) {
@@ -468,43 +241,6 @@ const BrowserHandler = () => {
}
}, [bucketName, loadingLocking, dispatch, displayListObjects]);
useEffect(() => {
// when a bucket param changes, (i.e /browser/:bucketName), re-init e.g with KBar, this should not apply for resources prefixes.
const permitItems = permissionItems(bucketName, "", allowResources || []);
if (bucketName && (!permitItems || permitItems.length === 0)) {
dispatch(resetMessages());
dispatch(setLoadingRecords(true));
dispatch(setLoadingObjects(true));
let pathPrefix = "";
if (internalPaths) {
const decodedPath = decodeURLString(internalPaths);
// internalPaths are selected (file details), we split and get parent folder
if (selectedInternalPaths === internalPaths) {
pathPrefix = `${decodeURLString(internalPaths)
.split("/")
.slice(0, -1)
.join("/")}/`;
} else {
pathPrefix = decodedPath.endsWith("/")
? decodedPath
: decodedPath + "/";
}
}
initWSRequest(pathPrefix, new Date());
}
}, [
bucketName,
dispatch,
initWSRequest,
allowResources,
internalPaths,
selectedInternalPaths,
]);
return (
<Fragment>
{!anonymousMode && <OBHeader bucketName={bucketName} />}
@@ -513,4 +249,4 @@ const BrowserHandler = () => {
);
};
export default withStyles(styles)(BrowserHandler);
export default BrowserHandler;

View File

@@ -46,7 +46,6 @@ import { setErrorSnackMessage, setHelpName } from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import { useSelector } from "react-redux";
import { selFeatures } from "../../consoleSlice";
import { setLoadingObjects } from "../../ObjectBrowser/objectBrowserSlice";
import PageHeaderWrapper from "../../Common/PageHeaderWrapper/PageHeaderWrapper";
import { api } from "../../../../api";
import { Bucket } from "../../../../api/consoleApi";
@@ -91,7 +90,6 @@ const ListBuckets = () => {
if (res.data) {
setLoading(false);
setRecords(res.data.buckets || []);
dispatch(setLoadingObjects(true));
} else if (res.error) {
setLoading(false);
dispatch(setErrorSnackMessage(errorToHandler(res.error)));

View File

@@ -15,10 +15,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import { Button, ClosePanelIcon, Grid } from "mds";
import makeStyles from "@mui/styles/makeStyles";
import { Box, Button, ClosePanelIcon } from "mds";
interface IDetailsListPanel {
open: boolean;
@@ -27,51 +24,44 @@ interface IDetailsListPanel {
children: React.ReactNode;
}
const useStyles = makeStyles((theme: Theme) =>
createStyles({
detailsList: {
borderColor: "#EAEDEE",
borderWidth: 0,
borderStyle: "solid",
borderRadius: 3,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
width: 0,
transitionDuration: "0.3s",
overflowX: "hidden",
overflowY: "auto",
position: "relative",
opacity: 0,
marginLeft: -1,
"&.open": {
width: 300,
minWidth: 300,
borderLeftWidth: 1,
opacity: 1,
},
"@media (max-width: 799px)": {
"&.open": {
width: "100%",
minWidth: "100%",
borderLeftWidth: 0,
},
},
},
}),
);
const DetailsListPanel = ({
open,
closePanel,
className = "",
children,
}: IDetailsListPanel) => {
const classes = useStyles();
return (
<Grid
item
className={`${classes.detailsList} ${open ? "open" : ""} ${className}`}
<Box
id={"details-panel"}
sx={{
borderColor: "#EAEDEE",
borderWidth: 0,
borderStyle: "solid",
borderRadius: 3,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
width: 0,
transitionDuration: "0.3s",
overflowX: "hidden",
overflowY: "auto",
position: "relative",
opacity: 0,
marginLeft: -1,
"&.open": {
width: 300,
minWidth: 300,
borderLeftWidth: 1,
opacity: 1,
},
"@media (max-width: 799px)": {
"&.open": {
width: "100%",
minWidth: "100%",
borderLeftWidth: 0,
},
},
}}
className={`${open ? "open" : ""} ${className}`}
>
<Button
variant={"text"}
@@ -90,7 +80,7 @@ const DetailsListPanel = ({
}}
/>
{children}
</Grid>
</Box>
);
};

View File

@@ -22,17 +22,17 @@ import React, {
useRef,
useState,
} from "react";
import { useSelector } from "react-redux";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import { useDropzone } from "react-dropzone";
import { Theme } from "@mui/material/styles";
import get from "lodash/get";
import {
AccessRuleIcon,
ActionsList,
Box,
BucketsIcon,
Button,
Checkbox,
DeleteIcon,
DownloadIcon,
Grid,
HistoryIcon,
PageLayout,
PreviewIcon,
@@ -40,29 +40,22 @@ import {
ScreenTitle,
ShareIcon,
} from "mds";
import { api } from "api";
import { Badge } from "@mui/material"; // TODO: Remove this
import { errorToHandler } from "api/errors";
import { BucketQuota } from "api/consoleApi";
import { useSelector } from "react-redux";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import { useDropzone } from "react-dropzone";
import { DateTime } from "luxon";
import createStyles from "@mui/styles/createStyles";
import Grid from "@mui/material/Grid";
import get from "lodash/get";
import {
decodeURLString,
encodeURLString,
niceBytesInt,
} from "../../../../../../common/utils";
import {
actionsTray,
containerForHeader,
objectBrowserCommon,
objectBrowserExtras,
searchField,
tableStyles,
} from "../../../../Common/FormComponents/common/styleLibrary";
import { Badge } from "@mui/material";
import BrowserBreadcrumbs from "../../../../ObjectBrowser/BrowserBreadcrumbs";
import { AllowedPreviews, previewObjectType } from "../utils";
import { ErrorResponseHandler } from "../../../../../../common/types";
import { AppState, useAppDispatch } from "../../../../../../store";
import {
IAM_SCOPES,
@@ -72,18 +65,16 @@ import {
hasPermission,
SecureComponent,
} from "../../../../../../common/SecureComponent";
import withSuspense from "../../../../Common/Components/withSuspense";
import UploadFilesButton from "../../UploadFilesButton";
import DetailsListPanel from "./DetailsListPanel";
import ObjectDetailPanel from "./ObjectDetailPanel";
import VersionsNavigator from "../ObjectDetails/VersionsNavigator";
import CheckboxWrapper from "../../../../Common/FormComponents/CheckboxWrapper/CheckboxWrapper";
import {
setErrorSnackMessage,
setSnackBarMessage,
} from "../../../../../../systemSlice";
import { isVersionedMode } from "../../../../../../utils/validationFunctions";
import {
extractFileExtn,
getPolicyAllowedFileExtensions,
getSessionGrantsWildCard,
} from "../../UploadPermissionUtils";
import {
makeid,
removeTrace,
@@ -99,15 +90,12 @@ import {
resetRewind,
setAnonymousAccessOpen,
setDownloadRenameModal,
setLoadingObjects,
setLoadingRecords,
setLoadingVersions,
setNewObject,
setObjectDetailsView,
setPreviewOpen,
setReloadObjectsList,
setRetentionConfig,
setSearchObjects,
setSelectedBucket,
setSelectedObjects,
setSelectedObjectView,
setSelectedPreview,
@@ -116,34 +104,28 @@ import {
setVersionsModeEnabled,
updateProgress,
} from "../../../../ObjectBrowser/objectBrowserSlice";
import makeStyles from "@mui/styles/makeStyles";
import {
selBucketDetailsInfo,
selBucketDetailsLoading,
setBucketDetailsLoad,
setBucketInfo,
} from "../../../BucketDetails/bucketDetailsSlice";
import RenameLongFileName from "../../../../ObjectBrowser/RenameLongFilename";
import TooltipWrapper from "../../../../Common/TooltipWrapper/TooltipWrapper";
import ListObjectsTable from "./ListObjectsTable";
import {
downloadSelected,
openAnonymousAccess,
openPreview,
openShare,
} from "../../../../ObjectBrowser/objectBrowserThunks";
import withSuspense from "../../../../Common/Components/withSuspense";
import UploadFilesButton from "../../UploadFilesButton";
import DetailsListPanel from "./DetailsListPanel";
import ObjectDetailPanel from "./ObjectDetailPanel";
import VersionsNavigator from "../ObjectDetails/VersionsNavigator";
import RenameLongFileName from "../../../../ObjectBrowser/RenameLongFilename";
import TooltipWrapper from "../../../../Common/TooltipWrapper/TooltipWrapper";
import ListObjectsTable from "./ListObjectsTable";
import FilterObjectsSB from "../../../../ObjectBrowser/FilterObjectsSB";
import AddAccessRule from "../../../BucketDetails/AddAccessRule";
import { isVersionedMode } from "../../../../../../utils/validationFunctions";
import { api } from "api";
import { errorToHandler } from "api/errors";
import { BucketQuota } from "api/consoleApi";
import {
extractFileExtn,
getPolicyAllowedFileExtensions,
getSessionGrantsWildCard,
} from "../../UploadPermissionUtils";
const DeleteMultipleObjects = withSuspense(
React.lazy(() => import("./DeleteMultipleObjects")),
@@ -156,82 +138,26 @@ const PreviewFileModal = withSuspense(
React.lazy(() => import("../Preview/PreviewFileModal")),
);
const useStyles = makeStyles((theme: Theme) =>
createStyles({
badgeOverlap: {
"& .MuiBadge-badge": {
top: 10,
right: 1,
width: 5,
height: 5,
minWidth: 5,
},
},
...tableStyles,
...actionsTray,
...searchField,
searchField: {
...searchField.searchField,
maxWidth: 380,
},
screenTitleContainer: {
border: "#EAEDEE 1px solid",
padding: "0 5px",
},
labelStyle: {
color: "#969FA8",
fontSize: "12px",
},
breadcrumbsContainer: {
padding: "12px 14px 5px",
},
fullContainer: {
width: "100%",
position: "relative",
"&.detailsOpen": {
"@media (max-width: 799px)": {
display: "none",
},
},
},
hideListOnSmall: {
"@media (max-width: 799px)": {
display: "none",
},
},
actionsSection: {
display: "flex",
justifyContent: "space-between",
width: "100%",
},
...objectBrowserExtras,
...objectBrowserCommon,
...containerForHeader,
}),
);
const baseDnDStyle = {
borderWidth: 2,
borderRadius: 2,
borderColor: "#eeeeee",
borderColor: "transparent",
outline: "none",
};
const activeDnDStyle = {
borderStyle: "dashed",
backgroundColor: "#fafafa",
backgroundColor: "transparent",
borderColor: "#2196f3",
};
const acceptDnDStyle = {
borderStyle: "dashed",
backgroundColor: "#fafafa",
backgroundColor: "transparent",
borderColor: "#00e676",
};
const ListObjects = () => {
const classes = useStyles();
const dispatch = useAppDispatch();
const params = useParams();
const navigate = useNavigate();
@@ -246,7 +172,6 @@ const ListObjects = () => {
const versionsMode = useSelector(
(state: AppState) => state.objectBrowser.versionsMode,
);
const showDeleted = useSelector(
(state: AppState) => state.objectBrowser.showDeleted,
);
@@ -256,13 +181,12 @@ const ListObjects = () => {
const selectedInternalPaths = useSelector(
(state: AppState) => state.objectBrowser.selectedInternalPaths,
);
const loadingObjects = useSelector(
(state: AppState) => state.objectBrowser.loadingObjects,
const requestInProgress = useSelector(
(state: AppState) => state.objectBrowser.requestInProgress,
);
const simplePath = useSelector(
(state: AppState) => state.objectBrowser.simplePath,
);
const versioningConfig = useSelector(
(state: AppState) => state.objectBrowser.versionInfo,
);
@@ -300,13 +224,12 @@ const ListObjects = () => {
const [canShareFile, setCanShareFile] = useState<boolean>(false);
const [canPreviewFile, setCanPreviewFile] = useState<boolean>(false);
const [quota, setQuota] = useState<BucketQuota | null>(null);
const [metaData, setMetaData] = useState<any>(null);
const [isMetaDataLoaded, setIsMetaDataLoaded] = useState(false);
const isVersioningApplied = isVersionedMode(versioningConfig.status);
const bucketName = params.bucketName || "";
const bucketName = params.bucketName || "";
const pathSegment = location.pathname.split(`/browser/${bucketName}/`);
const internalPaths = pathSegment.length === 2 ? pathSegment[1] : "";
@@ -399,12 +322,6 @@ const ListObjects = () => {
}
}, [bucketName, selectedObjects, fetchMetadata]);
useEffect(() => {
dispatch(setSearchObjects(""));
dispatch(setLoadingObjects(true));
dispatch(setSelectedObjects([]));
}, [simplePath, dispatch]);
useEffect(() => {
if (rewindEnabled) {
if (bucketToRewind !== bucketName) {
@@ -414,8 +331,6 @@ const ListObjects = () => {
}
}, [rewindEnabled, bucketToRewind, bucketName, dispatch]);
// END OF WS HANDLERS
useEffect(() => {
if (folderUpload.current !== null) {
folderUpload.current.setAttribute("directory", "");
@@ -477,11 +392,11 @@ const ListObjects = () => {
if (
selectedObjects.length === 0 &&
selectedInternalPaths === null &&
!loadingObjects
!requestInProgress
) {
dispatch(setObjectDetailsView(false));
}
}, [selectedObjects, selectedInternalPaths, dispatch, loadingObjects]);
}, [selectedObjects, selectedInternalPaths, dispatch, requestInProgress]);
useEffect(() => {
if (!iniLoad) {
@@ -492,20 +407,19 @@ const ListObjects = () => {
// bucket info
useEffect(() => {
if ((loadingObjects || loadingBucket) && !anonymousMode) {
if ((requestInProgress || loadingBucket) && !anonymousMode) {
api.buckets
.bucketInfo(bucketName)
.then((res) => {
dispatch(setBucketDetailsLoad(false));
dispatch(setBucketInfo(res.data));
dispatch(setSelectedBucket(bucketName));
})
.catch((err) => {
dispatch(setBucketDetailsLoad(false));
dispatch(setErrorSnackMessage(errorToHandler(err)));
});
}
}, [bucketName, loadingBucket, dispatch, anonymousMode, loadingObjects]);
}, [bucketName, loadingBucket, dispatch, anonymousMode, requestInProgress]);
// Load retention Config
@@ -528,7 +442,7 @@ const ListObjects = () => {
if (refresh) {
dispatch(setSnackBarMessage(`Objects deleted successfully.`));
dispatch(setSelectedObjects([]));
dispatch(setLoadingObjects(true));
dispatch(setReloadObjectsList(true));
}
};
@@ -544,7 +458,7 @@ const ListObjects = () => {
e.preventDefault();
var newFiles: File[] = [];
for (var i = 0; i < e.target.files.length; i++) {
for (let i = 0; i < e.target.files.length; i++) {
newFiles.push(e.target.files[i]);
}
uploadObject(newFiles, "");
@@ -637,7 +551,7 @@ const ListObjects = () => {
};
xhr.withCredentials = false;
xhr.onload = function (event) {
xhr.onload = function () {
// resolve promise only when HTTP code is ok
if (xhr.status >= 200 && xhr.status < 300) {
dispatch(completeObject(identity));
@@ -669,7 +583,7 @@ const ListObjects = () => {
}
};
xhr.upload.addEventListener("error", (event) => {
xhr.upload.addEventListener("error", () => {
reject(errorMessage);
dispatch(
failObject({
@@ -703,7 +617,7 @@ const ListObjects = () => {
};
xhr.onloadend = () => {
if (files.length === 0) {
dispatch(setLoadingObjects(true));
dispatch(setReloadObjectsList(true));
}
};
xhr.onabort = () => {
@@ -756,8 +670,7 @@ const ListObjects = () => {
dispatch(setErrorSnackMessage(err));
}
// We force objects list reload after all promises were handled
dispatch(setLoadingObjects(true));
dispatch(setSelectedObjects([]));
dispatch(setReloadObjectsList(true));
});
};
@@ -771,14 +684,13 @@ const ListObjects = () => {
if (acceptedFiles && acceptedFiles.length > 0 && canUpload) {
let newFolderPath: string = acceptedFiles[0].path;
//Should we filter by allowed file extensions if any?.
let allowedFiles = [];
let allowedFiles = acceptedFiles;
if (allowedFileExtensions.length > 0) {
allowedFiles = acceptedFiles.filter((file) => {
const fileExtn = extractFileExtn(file.name);
return allowedFileExtensions.includes(fileExtn);
});
} else {
allowedFiles = acceptedFiles;
}
if (allowedFiles.length) {
@@ -885,10 +797,9 @@ const ListObjects = () => {
}
dispatch(setObjectDetailsView(false));
dispatch(setSelectedObjects([]));
if (forceRefresh) {
dispatch(setLoadingObjects(true));
dispatch(setReloadObjectsList(true));
}
};
@@ -1049,7 +960,7 @@ const ListObjects = () => {
<FilterObjectsSB />
</div>
)}
<Grid item xs={12} className={classes.screenTitleContainer}>
<Box withBorders sx={{ padding: "0 5px" }}>
<ScreenTitle
icon={
<span>
@@ -1059,9 +970,18 @@ const ListObjects = () => {
title={bucketName}
subTitle={
!anonymousMode ? (
<Fragment>
<span className={classes.detailsSpacer}>
Created on:&nbsp;&nbsp;
<Box
sx={{
"& .detailsSpacer": {
marginRight: 18,
"@media (max-width: 600px)": {
marginRight: 0,
},
},
}}
>
<span className={"detailsSpacer"}>
Created on:&nbsp;
<strong>
{bucketInfo?.creation_date
? createdTime.toFormat(
@@ -1070,13 +990,13 @@ const ListObjects = () => {
: ""}
</strong>
</span>
<span className={classes.detailsSpacer}>
Access:&nbsp;&nbsp;&nbsp;
<span className={"detailsSpacer"}>
Access:&nbsp;&nbsp;
<strong>{bucketInfo?.access || ""}</strong>
</span>
{bucketInfo && (
<Fragment>
<span className={classes.detailsSpacer}>
<span className={"detailsSpacer"}>
{bucketInfo.size && (
<Fragment>{niceBytesInt(bucketInfo.size)}</Fragment>
)}
@@ -1098,7 +1018,7 @@ const ListObjects = () => {
</span>
</Fragment>
)}
</Fragment>
</Box>
) : null
}
actions={
@@ -1114,7 +1034,7 @@ const ListObjects = () => {
color="secondary"
variant="dot"
invisible={!rewindEnabled}
className={classes.badgeOverlap}
className={""}
sx={{ height: 16 }}
>
<HistoryIcon
@@ -1153,8 +1073,7 @@ const ListObjects = () => {
dispatch(setLoadingVersions(true));
} else {
dispatch(resetMessages());
dispatch(setLoadingRecords(true));
dispatch(setLoadingObjects(true));
dispatch(setReloadObjectsList(true));
}
}}
disabled={
@@ -1204,17 +1123,24 @@ const ListObjects = () => {
}
bottomBorder={false}
/>
</Grid>
</Box>
<div
id="object-list-wrapper"
{...getRootProps({ style: { ...dndStyles } })}
>
<input {...getInputProps()} />
<Grid
item
xs={12}
className={classes.tableBlock}
sx={{ border: "#EAEDEE 1px solid", borderTop: 0 }}
<Box
withBorders
sx={{
display: "flex",
borderTop: 0,
padding: 0,
"& .hideListOnSmall": {
"@media (max-width: 799px)": {
display: "none",
},
},
}}
>
{versionsMode ? (
<Fragment>
@@ -1237,37 +1163,52 @@ const ListObjects = () => {
<Grid
item
xs={12}
className={`${classes.fullContainer} ${
detailsOpen ? "detailsOpen" : ""
} `}
sx={{
width: "100%",
position: "relative",
"&.detailsOpen": {
"@media (max-width: 799px)": {
display: "none",
},
},
}}
className={detailsOpen ? "detailsOpen" : ""}
>
{!anonymousMode && (
<Grid item xs={12} className={classes.breadcrumbsContainer}>
<Grid
item
xs={12}
sx={{
padding: "12px 14px 5px",
}}
>
<BrowserBreadcrumbs
bucketName={bucketName}
internalPaths={pageTitle}
additionalOptions={
!isVersioningApplied || rewindEnabled ? null : (
<div>
<CheckboxWrapper
name={"deleted_objects"}
id={"showDeletedObjects"}
value={"deleted_on"}
label={"Show deleted objects"}
onChange={setDeletedAction}
checked={showDeleted}
overrideLabelClasses={classes.labelStyle}
className={classes.overrideShowDeleted}
noTopMargin
/>
</div>
<Checkbox
name={"deleted_objects"}
id={"showDeletedObjects"}
value={"deleted_on"}
label={"Show deleted objects"}
onChange={setDeletedAction}
checked={showDeleted}
sx={{
marginLeft: 5,
"@media (max-width: 600px)": {
marginLeft: 0,
flexDirection: "row" as const,
},
}}
/>
)
}
hidePathButton={false}
/>
</Grid>
)}
<ListObjectsTable internalPaths={selectedInternalPaths} />
<ListObjectsTable />
</Grid>
</SecureComponent>
)}
@@ -1285,7 +1226,7 @@ const ListObjects = () => {
closePanel={() => {
onClosePanel(false);
}}
className={`${versionsMode ? classes.hideListOnSmall : ""}`}
className={`${versionsMode ? "hideListOnSmall" : ""}`}
>
{selectedObjects.length > 0 && (
<ActionsList
@@ -1305,7 +1246,7 @@ const ListObjects = () => {
</DetailsListPanel>
</SecureComponent>
)}
</Grid>
</Box>
</div>
</PageLayout>
</Fragment>

View File

@@ -24,10 +24,9 @@ import { AppState, useAppDispatch } from "../../../../../../store";
import { selFeatures } from "../../../../consoleSlice";
import { encodeURLString } from "../../../../../../common/utils";
import {
setIsOpeningOD,
setLoadingObjects,
setLoadingVersions,
setObjectDetailsView,
setReloadObjectsList,
setSelectedObjects,
setSelectedObjectView,
} from "../../../../ObjectBrowser/objectBrowserSlice";
@@ -73,11 +72,7 @@ const useStyles = makeStyles((theme: Theme) =>
}),
);
interface IListObjectTable {
internalPaths: string | null;
}
const ListObjectsTable = ({ internalPaths }: IListObjectTable) => {
const ListObjectsTable = () => {
const classes = useStyles();
const dispatch = useAppDispatch();
const params = useParams();
@@ -94,8 +89,8 @@ const ListObjectsTable = ({ internalPaths }: IListObjectTable) => {
(state: AppState) => state.objectBrowser.objectDetailsOpen,
);
const loadingObjects = useSelector(
(state: AppState) => state.objectBrowser.loadingObjects,
const requestInProgress = useSelector(
(state: AppState) => state.objectBrowser.requestInProgress,
);
const features = useSelector(selFeatures);
@@ -154,11 +149,7 @@ const ListObjectsTable = ({ internalPaths }: IListObjectTable) => {
}`;
// for anonymous start download
if (
anonymousMode &&
internalPaths !== null &&
!object.name?.endsWith("/")
) {
if (anonymousMode && !object.name?.endsWith("/")) {
downloadObject(
dispatch,
bucketName,
@@ -174,7 +165,6 @@ const ListObjectsTable = ({ internalPaths }: IListObjectTable) => {
if (!anonymousMode) {
dispatch(setObjectDetailsView(true));
dispatch(setLoadingVersions(true));
dispatch(setIsOpeningOD(true));
}
dispatch(
setSelectedObjectView(
@@ -195,7 +185,7 @@ const ListObjectsTable = ({ internalPaths }: IListObjectTable) => {
const newSortDirection = get(sortData, "sortDirection", "DESC");
setCurrentSortField(sortData.sortBy);
setSortDirection(newSortDirection);
dispatch(setLoadingObjects(true));
dispatch(setReloadObjectsList(true));
};
const selectAllItems = () => {
@@ -255,7 +245,7 @@ const ListObjectsTable = ({ internalPaths }: IListObjectTable) => {
<DataTable
itemActions={tableActions}
columns={rewindEnabled ? rewindModeColumns : listModeColumns}
isLoading={loadingObjects}
isLoading={requestInProgress}
entityName="Objects"
idField="name"
records={payload}

View File

@@ -15,14 +15,15 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment, useEffect, useState } from "react";
import get from "lodash/get";
import { useSelector } from "react-redux";
import { Box } from "@mui/material";
import { withStyles } from "@mui/styles";
import {
ActionsList,
Box,
Button,
DeleteIcon,
DownloadIcon,
Grid,
InspectMenuIcon,
LegalHoldIcon,
Loader,
@@ -35,17 +36,10 @@ import {
TagsIcon,
VersionsIcon,
} from "mds";
import createStyles from "@mui/styles/createStyles";
import get from "lodash/get";
import Grid from "@mui/material/Grid";
import {
actionsTray,
detailsPanel,
spacingUtils,
textStyleUtils,
} from "../../../../Common/FormComponents/common/styleLibrary";
import { api } from "api";
import { downloadObject } from "../../../../ObjectBrowser/utils";
import { BucketObject, BucketVersioningResponse } from "api/consoleApi";
import { AllowedPreviews, previewObjectType } from "../utils";
import {
decodeURLString,
niceBytes,
@@ -57,19 +51,10 @@ import {
permissionTooltipHelper,
} from "../../../../../../common/SecureComponent/permissions";
import { AppState, useAppDispatch } from "../../../../../../store";
import ShareFile from "../ObjectDetails/ShareFile";
import SetRetention from "../ObjectDetails/SetRetention";
import DeleteObject from "../ListObjects/DeleteObject";
import SetLegalHoldModal from "../ObjectDetails/SetLegalHoldModal";
import {
hasPermission,
SecureComponent,
} from "../../../../../../common/SecureComponent";
import PreviewFileModal from "../Preview/PreviewFileModal";
import ObjectMetaData from "../ObjectDetails/ObjectMetaData";
import { displayFileIconName } from "./utils";
import TagsModal from "../ObjectDetails/TagsModal";
import InspectObject from "./InspectObject";
import { selDistSet } from "../../../../../../systemSlice";
import {
setLoadingObjectInfo,
@@ -77,40 +62,17 @@ import {
setSelectedVersion,
setVersionsModeEnabled,
} from "../../../../ObjectBrowser/objectBrowserSlice";
import { displayFileIconName } from "./utils";
import PreviewFileModal from "../Preview/PreviewFileModal";
import ObjectMetaData from "../ObjectDetails/ObjectMetaData";
import ShareFile from "../ObjectDetails/ShareFile";
import SetRetention from "../ObjectDetails/SetRetention";
import DeleteObject from "../ListObjects/DeleteObject";
import SetLegalHoldModal from "../ObjectDetails/SetLegalHoldModal";
import TagsModal from "../ObjectDetails/TagsModal";
import InspectObject from "./InspectObject";
import RenameLongFileName from "../../../../ObjectBrowser/RenameLongFilename";
import TooltipWrapper from "../../../../Common/TooltipWrapper/TooltipWrapper";
import { downloadObject } from "../../../../ObjectBrowser/utils";
import { BucketObject, BucketVersioningResponse } from "api/consoleApi";
import { api } from "api";
const styles = () =>
createStyles({
ObjectDetailsTitle: {
display: "flex",
alignItems: "center",
"& .min-icon": {
width: 26,
height: 26,
minWidth: 26,
minHeight: 26,
},
},
objectNameContainer: {
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
alignItems: "center",
marginLeft: 10,
},
capitalizeFirst: {
textTransform: "capitalize",
},
...actionsTray,
...spacingUtils,
...textStyleUtils,
...detailsPanel,
});
const emptyFile: BucketObject = {
is_latest: true,
@@ -125,7 +87,6 @@ const emptyFile: BucketObject = {
};
interface IObjectDetailPanelProps {
classes: any;
internalPaths: string;
bucketName: string;
versioningInfo: BucketVersioningResponse;
@@ -134,7 +95,6 @@ interface IObjectDetailPanelProps {
}
const ObjectDetailPanel = ({
classes,
internalPaths,
bucketName,
versioningInfo,
@@ -652,14 +612,40 @@ const ObjectDetailPanel = ({
{loadingObjectInfo ? (
<Fragment>{loaderForContainer}</Fragment>
) : (
<Fragment>
<Box
sx={{
"& .ObjectDetailsTitle": {
display: "flex",
alignItems: "center",
"& .min-icon": {
width: 26,
height: 26,
minWidth: 26,
minHeight: 26,
},
},
"& .objectNameContainer": {
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
alignItems: "center",
marginLeft: 10,
},
"& .capitalizeFirst": {
textTransform: "capitalize",
},
"& .detailContainer": {
padding: "0 22px",
marginBottom: 10,
fontSize: 14,
},
}}
>
<ActionsList
title={
<div className={classes.ObjectDetailsTitle}>
<div className={"ObjectDetailsTitle"}>
{displayFileIconName(objectName || "", true)}
<span className={classes.objectNameContainer}>
{objectName}
</span>
<span className={"objectNameContainer"}>{objectName}</span>
</div>
}
items={multiActionButtons}
@@ -710,19 +696,19 @@ const ObjectDetailPanel = ({
</Grid>
</TooltipWrapper>
<SimpleHeader icon={<ObjectInfoIcon />} label={"Object Info"} />
<Box className={classes.detailContainer}>
<Box className={"detailContainer"}>
<strong>Name:</strong>
<br />
<div style={{ overflowWrap: "break-word" }}>{objectName}</div>
</Box>
{selectedVersion !== "" && (
<Box className={classes.detailContainer}>
<Box className={"detailContainer"}>
<strong>Version ID:</strong>
<br />
{selectedVersion}
</Box>
)}
<Box className={classes.detailContainer}>
<Box className={"detailContainer"}>
<strong>Size:</strong>
<br />
{niceBytes(`${actualInfo.size || "0"}`)}
@@ -730,7 +716,7 @@ const ObjectDetailPanel = ({
{actualInfo.version_id &&
actualInfo.version_id !== "null" &&
selectedVersion === "" && (
<Box className={classes.detailContainer}>
<Box className={"detailContainer"}>
<strong>Versions:</strong>
<br />
{versions.length} version{versions.length !== 1 ? "s" : ""},{" "}
@@ -738,18 +724,18 @@ const ObjectDetailPanel = ({
</Box>
)}
{selectedVersion === "" && (
<Box className={classes.detailContainer}>
<Box className={"detailContainer"}>
<strong>Last Modified:</strong>
<br />
{calculateLastModifyTime(actualInfo.last_modified || "")}
</Box>
)}
<Box className={classes.detailContainer}>
<Box className={"detailContainer"}>
<strong>ETAG:</strong>
<br />
{actualInfo.etag || "N/A"}
</Box>
<Box className={classes.detailContainer}>
<Box className={"detailContainer"}>
<strong>Tags:</strong>
<br />
{tagKeys.length === 0
@@ -763,7 +749,7 @@ const ObjectDetailPanel = ({
);
})}
</Box>
<Box className={classes.detailContainer}>
<Box className={"detailContainer"}>
<SecureComponent
scopes={[
IAM_SCOPES.S3_GET_OBJECT_LEGAL_HOLD,
@@ -778,7 +764,7 @@ const ObjectDetailPanel = ({
</Fragment>
</SecureComponent>
</Box>
<Box className={classes.detailContainer}>
<Box className={"detailContainer"}>
<SecureComponent
scopes={[
IAM_SCOPES.S3_GET_OBJECT_RETENTION,
@@ -789,7 +775,7 @@ const ObjectDetailPanel = ({
<Fragment>
<strong>Retention Policy:</strong>
<br />
<span className={classes.capitalizeFirst}>
<span className={"capitalizeFirst"}>
{actualInfo.version_id && actualInfo.version_id !== "null" ? (
<Fragment>
{actualInfo.retention_mode
@@ -810,17 +796,17 @@ const ObjectDetailPanel = ({
{!actualInfo.is_delete_marker && (
<Fragment>
<SimpleHeader label={"Metadata"} icon={<MetadataIcon />} />
<Box className={classes.detailContainer}>
<Box className={"detailContainer"}>
{actualInfo && metaData ? (
<ObjectMetaData metaData={metaData} linear />
) : null}
</Box>
</Fragment>
)}
</Fragment>
</Box>
)}
</Fragment>
);
};
export default withStyles(styles)(ObjectDetailPanel);
export default ObjectDetailPanel;

View File

@@ -15,18 +15,17 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { DateTime } from "luxon";
import { Button, Grid, ProgressBar, Switch } from "mds";
import { useSelector } from "react-redux";
import ModalWrapper from "../../../../Common/ModalWrapper/ModalWrapper";
import DateTimePickerWrapper from "../../../../Common/FormComponents/DateTimePickerWrapper/DateTimePickerWrapper";
import { AppState, useAppDispatch } from "../../../../../../store";
import {
resetRewind,
setLoadingObjects,
setReloadObjectsList,
setRewindEnable,
} from "../../../../ObjectBrowser/objectBrowserSlice";
import { DateTime } from "luxon";
interface IRewindEnable {
closeModalAndRefresh: () => void;
@@ -76,7 +75,7 @@ const RewindEnable = ({
}),
);
}
dispatch(setLoadingObjects(true));
dispatch(setReloadObjectsList(true));
closeModalAndRefresh();
};

View File

@@ -213,6 +213,10 @@ const Console = ({ classes }: IConsoleProps) => {
const kmsIsEnabled = (features && features.includes("kms")) || false;
const obOnly = !!features?.includes("object-browser-only");
useEffect(() => {
dispatch({ type: "socket/OBConnect" });
}, [dispatch]);
const restartServer = () => {
dispatch(serverIsLoading(true));
api.service

View File

@@ -58,7 +58,6 @@ import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper";
import { Bucket } from "../../../api/consoleApi";
import { api } from "../../../api";
import { errorToHandler } from "../../../api/errors";
import { setLoadingObjects } from "./objectBrowserSlice";
import HelpMenu from "../HelpMenu";
const useStyles = makeStyles((theme: Theme) =>
@@ -104,7 +103,6 @@ const OBListBuckets = () => {
if (res.data) {
setLoading(false);
setRecords(res.data.buckets || []);
dispatch(setLoadingObjects(true));
}
})
.catch((err) => {

View File

@@ -34,7 +34,8 @@ const defaultRewind = {
const initialState: ObjectBrowserState = {
selectedBucket: "",
versionsMode: false,
loadingObjects: true,
reloadObjectsList: false,
requestInProgress: true,
objectDetailsOpen: false,
loadingVersions: true,
loadingObjectInfo: true,
@@ -59,7 +60,6 @@ const initialState: ObjectBrowserState = {
simplePath: null,
// object browser
records: [],
loadRecords: true,
loadingVersioning: true,
versionInfo: {},
lockingEnabled: false,
@@ -69,7 +69,6 @@ const initialState: ObjectBrowserState = {
selectedPreview: null,
previewOpen: false,
shareFileModalOpen: false,
isOpeningObjectDetail: false,
anonymousAccessOpen: false,
retentionConfig: {
mode: undefined,
@@ -247,8 +246,8 @@ export const objectBrowserSlice = createSlice({
setSearchObjects: (state, action: PayloadAction<string>) => {
state.searchObjects = action.payload;
},
setLoadingObjects: (state, action: PayloadAction<boolean>) => {
state.loadingObjects = action.payload;
setRequestInProgress: (state, action: PayloadAction<boolean>) => {
state.requestInProgress = action.payload;
},
setSearchVersions: (state, action: PayloadAction<string>) => {
state.searchVersions = action.payload;
@@ -313,8 +312,13 @@ export const objectBrowserSlice = createSlice({
resetMessages: (state) => {
state.records = [];
},
setLoadingRecords: (state, action: PayloadAction<boolean>) => {
state.loadRecords = action.payload;
setReloadObjectsList: (state, action: PayloadAction<boolean>) => {
state.reloadObjectsList = action.payload;
// If we initialize a request, then we must clean the records list
if (action.payload) {
state.records = [];
}
},
setSelectedObjects: (state, action: PayloadAction<string[]>) => {
state.selectedObjects = action.payload;
@@ -352,9 +356,6 @@ export const objectBrowserSlice = createSlice({
action.payload.objectInfo.size || 0;
}
},
setIsOpeningOD: (state, action: PayloadAction<boolean>) => {
state.isOpeningObjectDetail = action.payload;
},
setRetentionConfig: (
state,
action: PayloadAction<GetBucketRetentionConfig | null>,
@@ -373,7 +374,7 @@ export const objectBrowserSlice = createSlice({
errorInConnection: (state, action: PayloadAction<boolean>) => {
state.connectionError = action.payload;
if (action.payload) {
state.loadingObjects = false;
state.requestInProgress = false;
state.loadingObjectInfo = false;
state.objectDetailsOpen = false;
}
@@ -394,7 +395,7 @@ export const {
openList,
closeList,
setSearchObjects,
setLoadingObjects,
setRequestInProgress,
cancelObjectInList,
setSearchVersions,
setSelectedVersion,
@@ -418,9 +419,8 @@ export const {
setSelectedPreview,
setPreviewOpen,
setShareFileModalOpen,
setLoadingRecords,
setReloadObjectsList,
restoreLocalObjectList,
setIsOpeningOD,
setRetentionConfig,
setSelectedBucket,
setLongFileOpen,

View File

@@ -32,7 +32,8 @@ export interface ObjectBrowserState {
objectManager: ObjectManager;
searchObjects: string;
loadingVersions: boolean;
loadingObjects: boolean;
reloadObjectsList: boolean;
requestInProgress: boolean;
loadingObjectInfo: boolean;
versionsMode: boolean;
versionedFile: string;
@@ -43,7 +44,6 @@ export interface ObjectBrowserState {
selectedInternalPaths: string | null;
simplePath: string | null;
records: BucketObjectItem[];
loadRecords: boolean;
loadingVersioning: boolean;
versionInfo: BucketVersioningResponse;
lockingEnabled: boolean | undefined;
@@ -53,7 +53,6 @@ export interface ObjectBrowserState {
selectedPreview: BucketObjectItem | null;
previewOpen: boolean;
shareFileModalOpen: boolean;
isOpeningObjectDetail: boolean;
retentionConfig: GetBucketRetentionConfig | null;
longFileOpen: boolean;
anonymousAccessOpen: boolean;

View File

@@ -32,6 +32,10 @@ const LogoutPage = () => {
const deleteSession = () => {
clearSession();
dispatch(userLogged(false));
// Disconnect OB Websocket
dispatch({ type: "socket/OBDisconnect" });
localStorage.setItem("userLoggedIn", "");
localStorage.setItem("redirect-path", "");
dispatch(resetSession());
@@ -44,7 +48,7 @@ const LogoutPage = () => {
deleteSession();
})
.catch((err: ErrorResponseHandler) => {
console.log(err);
console.error(err);
deleteSession();
});
};

View File

@@ -13,6 +13,7 @@
//
// 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 { useDispatch } from "react-redux";
import { combineReducers, configureStore } from "@reduxjs/toolkit";
@@ -31,6 +32,9 @@ import createUserReducer from "./screens/Console/Users/AddUsersSlice";
import licenseReducer from "./screens/Console/License/licenseSlice";
import registerReducer from "./screens/Console/Support/registerSlice";
import destinationSlice from "./screens/Console/EventDestinations/destinationsSlice";
import { objectBrowserWSMiddleware } from "./websockets/objectBrowserWSMiddleware";
let objectsWS: WebSocket;
const rootReducer = combineReducers({
system: systemReducer,
@@ -52,6 +56,8 @@ const rootReducer = combineReducers({
export const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(objectBrowserWSMiddleware(objectsWS)),
});
if (process.env.NODE_ENV !== "production" && module.hot) {
@@ -60,7 +66,7 @@ if (process.env.NODE_ENV !== "production" && module.hot) {
});
}
export type AppState = ReturnType<typeof store.getState>;
export type AppState = ReturnType<typeof rootReducer>;
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = () => useDispatch<AppDispatch>();

View File

@@ -0,0 +1,231 @@
// This file is part of MinIO Console Server
// Copyright (c) 2023 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 get from "lodash/get";
import { Middleware } from "@reduxjs/toolkit";
import { AppState } from "../store";
import { wsProtocol } from "../utils/wsUtils";
import {
errorInConnection,
newMessage,
resetMessages,
setRecords,
setReloadObjectsList,
setRequestInProgress,
setSearchObjects,
setSelectedBucket,
setSelectedObjects,
setSimplePathHandler,
} from "../screens/Console/ObjectBrowser/objectBrowserSlice";
import {
WebsocketRequest,
WebsocketResponse,
} from "../screens/Console/Buckets/ListBuckets/Objects/ListObjects/types";
import { decodeURLString, encodeURLString } from "../common/utils";
import { permissionItems } from "../screens/Console/Buckets/ListBuckets/Objects/utils";
import { setErrorSnackMessage } from "../systemSlice";
let wsInFlight: boolean = false;
let currentRequestID: number = 0;
export const objectBrowserWSMiddleware = (
objectsWS: WebSocket,
): Middleware<{}, AppState> => {
return (storeApi) => (next) => (action) => {
const dispatch = storeApi.dispatch;
const storeState = storeApi.getState();
const allowResources = get(
storeState,
"console.session.allowResources",
null,
);
const bucketName = get(storeState, "objectBrowser.selectedBucket", "");
const { type } = action;
switch (type) {
case "socket/OBConnect":
const sessionInitialized = get(storeState, "system.loggedIn", false);
if (wsInFlight || !sessionInitialized) {
return;
}
wsInFlight = true;
const url = new URL(window.location.toString());
const isDev = process.env.NODE_ENV === "development";
const port = isDev ? "9090" : url.port;
// check if we are using base path, if not this always is `/`
const baseLocation = new URL(document.baseURI);
const baseUrl = baseLocation.pathname;
const wsProt = wsProtocol(url.protocol);
objectsWS = new WebSocket(
`${wsProt}://${url.hostname}:${port}${baseUrl}ws/objectManager`,
);
objectsWS.onopen = () => {
wsInFlight = false;
};
objectsWS.onmessage = (message) => {
const response: WebsocketResponse = JSON.parse(
message.data.toString(),
);
if (currentRequestID === response.request_id) {
// If response is not from current request, we can omit
if (response.request_id !== currentRequestID) {
return;
}
if (
response.error ===
"The Access Key Id you provided does not exist in our records."
) {
// Session expired.
window.location.reload();
} else if (response.error === "Access Denied.") {
const internalPathsPrefix = response.prefix;
let pathPrefix = "";
if (internalPathsPrefix) {
const decodedPath = decodeURLString(internalPathsPrefix);
pathPrefix = decodedPath.endsWith("/")
? decodedPath
: decodedPath + "/";
}
const permitItems = permissionItems(
response.bucketName || bucketName,
pathPrefix,
allowResources || [],
);
if (!permitItems || permitItems.length === 0) {
dispatch(
setErrorSnackMessage({
errorMessage: response.error,
detailedError: response.error,
}),
);
} else {
dispatch(setRequestInProgress(false));
dispatch(setRecords(permitItems));
}
return;
}
// This indicates final messages is received.
if (response.request_end) {
dispatch(setRequestInProgress(false));
return;
}
if (response.data) {
dispatch(setRequestInProgress(false));
dispatch(newMessage(response.data));
}
}
};
objectsWS.onclose = () => {
wsInFlight = false;
console.warn("Websocket Disconnected. Attempting Reconnection...");
// We reconnect after 3 seconds
setTimeout(() => dispatch({ type: "socket/OBConnect" }), 3000);
};
objectsWS.onerror = () => {
wsInFlight = false;
console.error(
"Error in websocket connection. Attempting reconnection...",
);
// Onclose will be triggered by specification, reconnect function will be executed there to avoid duplicated requests
};
break;
case "socket/OBRequest":
if (objectsWS && objectsWS.readyState === 1) {
try {
const newRequestID = currentRequestID + 1;
const dataPayload = action.payload;
dispatch(resetMessages());
dispatch(errorInConnection(false));
dispatch(setSimplePathHandler(dataPayload.path));
dispatch(setSelectedBucket(dataPayload.bucketName));
dispatch(setRequestInProgress(true));
dispatch(setReloadObjectsList(false));
dispatch(setSearchObjects(""));
dispatch(setSelectedObjects([]));
const request: WebsocketRequest = {
bucket_name: dataPayload.bucketName,
prefix: encodeURLString(dataPayload.path),
mode: dataPayload.rewindMode ? "rewind" : "objects",
date: dataPayload.date,
request_id: newRequestID,
};
objectsWS.send(JSON.stringify(request));
// We store the new ID for the requestID
currentRequestID = newRequestID;
} catch (e) {
console.error(e);
}
} else {
dispatch(setReloadObjectsList(false));
if (!wsInFlight) {
dispatch({ type: "socket/OBConnect" });
}
// Retry request after 1 second
setTimeout(
() =>
dispatch({ type: "socket/OBRequest", payload: action.payload }),
1000,
);
}
break;
case "socket/OBCancelLast":
const request: WebsocketRequest = {
mode: "cancel",
request_id: currentRequestID,
};
if (objectsWS && objectsWS.readyState === 1) {
objectsWS.send(JSON.stringify(request));
}
break;
case "socket/OBDisconnect":
objectsWS.close();
break;
default:
break;
}
return next(action);
};
};

View File

@@ -23,7 +23,7 @@ fixture("Delete Objects With Prefix Only policy").page(
"http://localhost:9090/",
);
export const sideBar = Selector("div.MuiGrid-root.MuiGrid-item");
export const sideBar = Selector("#details-panel");
export const sideBarDeleteButton = sideBar.find("button").withText("Delete");
const bucket1 = "test-1";
const test1BucketBrowseButton = testBucketBrowseButtonFor(bucket1);

View File

@@ -45,7 +45,7 @@ test
.navigateTo(`http://localhost:9090/browser`)
.click(testBucketBrowseButton)
.wait(1500)
.click(Selector("input").withAttribute("id", "showDeletedObjects"))
.click(Selector("label").withText("Show deleted objects"))
.click(
Selector(".ReactVirtualized__Table__rowColumn").withText(
"firstlevel",