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:
@@ -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;
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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:
|
||||
<Box
|
||||
sx={{
|
||||
"& .detailsSpacer": {
|
||||
marginRight: 18,
|
||||
"@media (max-width: 600px)": {
|
||||
marginRight: 0,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<span className={"detailsSpacer"}>
|
||||
Created on:
|
||||
<strong>
|
||||
{bucketInfo?.creation_date
|
||||
? createdTime.toFormat(
|
||||
@@ -1070,13 +990,13 @@ const ListObjects = () => {
|
||||
: ""}
|
||||
</strong>
|
||||
</span>
|
||||
<span className={classes.detailsSpacer}>
|
||||
Access:
|
||||
<span className={"detailsSpacer"}>
|
||||
Access:
|
||||
<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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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>();
|
||||
|
||||
231
portal-ui/src/websockets/objectBrowserWSMiddleware.ts
Normal file
231
portal-ui/src/websockets/objectBrowserWSMiddleware.ts
Normal 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);
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user