Skip to content

Commit dcf6a52

Browse files
authored
Inspect Object (#1663)
1 parent 25562bd commit dcf6a52

File tree

4 files changed

+297
-43
lines changed

4 files changed

+297
-43
lines changed
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// This file is part of MinIO Console Server
2+
// Copyright (c) 2022 MinIO, Inc.
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
import React, { useState } from "react";
18+
import { connect } from "react-redux";
19+
import withStyles from "@mui/styles/withStyles";
20+
import { setErrorSnackMessage } from "../../../../../../actions";
21+
import {decodeFileName, deleteCookie, getCookieValue, performDownload} from "../../../../../../common/utils";
22+
import FormSwitchWrapper from "../../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
23+
import ModalWrapper from "../../../../Common/ModalWrapper/ModalWrapper";
24+
import {InspectMenuIcon} from "../../../../../../icons/SidebarMenus";
25+
import Button from "@mui/material/Button";
26+
import Grid from "@mui/material/Grid";
27+
import {Theme} from "@mui/material/styles";
28+
import createStyles from "@mui/styles/createStyles";
29+
import {formFieldStyles, modalStyleUtils, spacingUtils} from "../../../../Common/FormComponents/common/styleLibrary";
30+
import {PasswordKeyIcon} from "../../../../../../icons";
31+
import {Box, DialogContentText} from "@mui/material";
32+
import KeyRevealer from "../../../../Tools/KeyRevealer";
33+
34+
const styles = (theme: Theme) =>
35+
createStyles({
36+
...formFieldStyles,
37+
...modalStyleUtils,
38+
...spacingUtils,
39+
});
40+
41+
interface IInspectObjectProps {
42+
classes: any;
43+
closeInspectModalAndRefresh: (refresh: boolean) => void;
44+
inspectOpen: boolean;
45+
inspectPath: string;
46+
volumeName: string;
47+
setErrorSnackMessage: typeof setErrorSnackMessage;
48+
}
49+
50+
const InspectObject = ({
51+
classes,
52+
closeInspectModalAndRefresh,
53+
inspectOpen,
54+
inspectPath,
55+
volumeName,
56+
setErrorSnackMessage,
57+
}: IInspectObjectProps) => {
58+
const onClose = () => closeInspectModalAndRefresh(false);
59+
const [isEncrypt, setIsEncrypt] = useState<boolean>(true);
60+
const [decryptionKey, setDecryptionKey] = useState<string>("");
61+
const [insFileName, setInsFileName] = useState<string>("");
62+
63+
if (!inspectPath) {
64+
return null;
65+
}
66+
const makeRequest = async (url: string) => {
67+
return await fetch(url, { method: "GET" });
68+
};
69+
70+
const performInspect = async () => {
71+
const file = encodeURIComponent(inspectPath+"/xl.meta");
72+
const volume = encodeURIComponent(volumeName);
73+
74+
const urlOfInspectApi = `/api/v1/admin/inspect?volume=${volume}&file=${file}&encrypt=${isEncrypt}`;
75+
76+
makeRequest(urlOfInspectApi)
77+
.then(async (res) => {
78+
if (!res.ok) {
79+
const resErr: any = await res.json();
80+
81+
setErrorSnackMessage({
82+
errorMessage: resErr.message,
83+
detailedError: resErr.code,
84+
});
85+
}
86+
const blob: Blob = await res.blob();
87+
88+
//@ts-ignore
89+
const filename = res.headers.get("content-disposition").split('"')[1];
90+
const decryptKey = getCookieValue(filename) || "";
91+
92+
performDownload(blob, filename);
93+
setInsFileName(filename);
94+
if (decryptKey === "") {
95+
onClose();
96+
return;
97+
}
98+
setDecryptionKey(decryptKey);
99+
})
100+
.catch((err) => {
101+
setErrorSnackMessage(err);
102+
});
103+
};
104+
105+
const onCloseDecKeyModal = () => {
106+
deleteCookie(insFileName);
107+
onClose();
108+
setDecryptionKey("");
109+
};
110+
111+
const onSubmit = (e: React.FormEvent) => {
112+
e.preventDefault();
113+
};
114+
115+
return (
116+
<React.Fragment>
117+
{!decryptionKey && <ModalWrapper
118+
modalOpen={inspectOpen}
119+
titleIcon={<InspectMenuIcon />}
120+
title={`Inspect Object`}
121+
onClose={onClose}
122+
>
123+
<form
124+
noValidate
125+
autoComplete="off"
126+
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
127+
onSubmit(e);
128+
}}
129+
>
130+
Would you like to encrypt{" "}
131+
<b>{decodeFileName(inspectPath)}</b>? <br />
132+
<FormSwitchWrapper
133+
label={"Encrypt"}
134+
indicatorLabels={["Yes", "No"]}
135+
checked={isEncrypt}
136+
value={"encrypt"}
137+
id="encrypt"
138+
name="encrypt"
139+
onChange={(e) => {
140+
setIsEncrypt(!isEncrypt);
141+
}}
142+
description=""
143+
/>
144+
<Grid item xs={12} className={classes.modalButtonBar}>
145+
<Button
146+
type="submit"
147+
variant="contained"
148+
color="primary"
149+
onClick={performInspect}
150+
>
151+
Inspect
152+
</Button>
153+
</Grid>
154+
</form>
155+
</ModalWrapper>}
156+
{decryptionKey ? (
157+
<ModalWrapper
158+
modalOpen={inspectOpen}
159+
title="Inspect Decryption Key"
160+
onClose={onCloseDecKeyModal}
161+
titleIcon={<PasswordKeyIcon />}
162+
>
163+
<DialogContentText>
164+
<Box>
165+
This will be displayed only once. It cannot be recovered.
166+
<br />
167+
Use secure medium to share this key.
168+
</Box>
169+
<Box>
170+
<KeyRevealer value={decryptionKey} />
171+
</Box>
172+
</DialogContentText>
173+
</ModalWrapper>
174+
) : null}
175+
</React.Fragment>
176+
);
177+
};
178+
179+
const mapDispatchToProps = {
180+
setErrorSnackMessage,
181+
};
182+
183+
const connector = connect(null, mapDispatchToProps);
184+
185+
export default withStyles(styles)(connector(InspectObject));

portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ObjectDetailPanel.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import {
5959
TagsIcon,
6060
VersionsIcon,
6161
} from "../../../../../../icons";
62+
import { InspectMenuIcon } from "../../../../../../icons/SidebarMenus";
6263
import { ShareIcon, DownloadIcon, DeleteIcon } from "../../../../../../icons";
6364
import api from "../../../../../../common/api";
6465
import ShareFile from "../ObjectDetails/ShareFile";
@@ -75,6 +76,7 @@ import ObjectMetaData from "../ObjectDetails/ObjectMetaData";
7576
import ActionsListSection from "./ActionsListSection";
7677
import { displayFileIconName } from "./utils";
7778
import TagsModal from "../ObjectDetails/TagsModal";
79+
import InspectObject from "./InspectObject";
7880

7981
const styles = () =>
8082
createStyles({
@@ -186,6 +188,7 @@ const ObjectDetailPanel = ({
186188
const [retentionModalOpen, setRetentionModalOpen] = useState<boolean>(false);
187189
const [tagModalOpen, setTagModalOpen] = useState<boolean>(false);
188190
const [legalholdOpen, setLegalholdOpen] = useState<boolean>(false);
191+
const [inspectModalOpen, setInspectModalOpen] = useState<boolean>(false);
189192
const [actualInfo, setActualInfo] = useState<IFileInfo | null>(null);
190193
const [allInfoElements, setAllInfoElements] = useState<IFileInfo[]>([]);
191194
const [objectToShare, setObjectToShare] = useState<IFileInfo | null>(null);
@@ -344,6 +347,13 @@ const ObjectDetailPanel = ({
344347
}
345348
};
346349

350+
const closeInspectModal = (reloadObjectData: boolean) => {
351+
setInspectModalOpen(false);
352+
if (reloadObjectData) {
353+
setLoadObjectData(true);
354+
}
355+
};
356+
347357
const closeLegalholdModal = (reload: boolean) => {
348358
setLegalholdOpen(false);
349359
if (reload) {
@@ -439,6 +449,18 @@ const ObjectDetailPanel = ({
439449
icon: <TagsIcon />,
440450
tooltip: "Change Tags for this File",
441451
},
452+
{
453+
action: () => {
454+
setInspectModalOpen(true);
455+
},
456+
label: "Inspect",
457+
disabled:
458+
!!actualInfo.is_delete_marker ||
459+
extensionPreview(currentItem) === "none" ||
460+
selectedVersion !== "",
461+
icon: <InspectMenuIcon />,
462+
tooltip: "Inspect this file",
463+
},
442464
{
443465
action: () => {
444466
setVersionsModeEnabled(!versionsMode, objectName);
@@ -448,6 +470,7 @@ const ObjectDetailPanel = ({
448470
disabled: !(actualInfo.version_id && actualInfo.version_id !== "null"),
449471
tooltip: "Display Versions for this file",
450472
},
473+
451474
];
452475

453476
const calculateLastModifyTime = (lastModified: string) => {
@@ -527,6 +550,14 @@ const ObjectDetailPanel = ({
527550
onCloseAndUpdate={closeAddTagModal}
528551
/>
529552
)}
553+
{inspectModalOpen && actualInfo && (
554+
<InspectObject
555+
inspectOpen={inspectModalOpen}
556+
volumeName={bucketName}
557+
inspectPath={actualInfo.name}
558+
closeInspectModalAndRefresh={closeInspectModal}
559+
/>
560+
)}
530561

531562
{!actualInfo && (
532563
<Grid item xs={12}>

portal-ui/src/screens/Console/Tools/Inspect.tsx

Lines changed: 18 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,26 @@
1+
// This file is part of MinIO Console Server
2+
// Copyright (c) 2022 MinIO, Inc.
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
117
import React, { Fragment, useEffect, useState } from "react";
218
import { Box, Button, DialogContentText } from "@mui/material";
319
import PageHeader from "../Common/PageHeader/PageHeader";
420
import PageLayout from "../Common/Layout/PageLayout";
521
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
622
import FormSwitchWrapper from "../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
7-
import { CopyIcon, FileBookIcon, PasswordKeyIcon } from "../../../icons";
23+
import { FileBookIcon, PasswordKeyIcon } from "../../../icons";
824
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
925
import { Theme } from "@mui/material/styles";
1026
import createStyles from "@mui/styles/createStyles";
@@ -24,6 +40,7 @@ import {
2440
import DistributedOnly from "../Common/DistributedOnly/DistributedOnly";
2541
import { AppState } from "../../../store";
2642
import { InspectMenuIcon } from "../../../icons/SidebarMenus";
43+
import KeyRevealer from "./KeyRevealer";
2744

2845
const styles = (theme: Theme) =>
2946
createStyles({
@@ -34,48 +51,6 @@ const styles = (theme: Theme) =>
3451
...modalStyleUtils,
3552
});
3653

37-
const KeyRevealer = ({ value }: { value: string }) => {
38-
const [shown, setShown] = React.useState(false);
39-
40-
return (
41-
<Box
42-
sx={{
43-
display: "flex",
44-
alignItems: "center",
45-
flexFlow: {
46-
sm: "row",
47-
xs: "column",
48-
},
49-
}}
50-
>
51-
<InputBoxWrapper
52-
id="inspect-dec-key"
53-
name="inspect-dec-key"
54-
placeholder=""
55-
label=""
56-
type={shown ? "text" : "password"}
57-
onChange={() => {}}
58-
value={value}
59-
overlayIcon={<CopyIcon />}
60-
extraInputProps={{
61-
readOnly: true,
62-
}}
63-
overlayAction={() => navigator.clipboard.writeText(value)}
64-
/>
65-
66-
<Button
67-
sx={{
68-
marginLeft: "10px",
69-
}}
70-
variant="contained"
71-
onClick={() => setShown(!shown)}
72-
>
73-
Show/Hide
74-
</Button>
75-
</Box>
76-
);
77-
};
78-
7954
const mapState = (state: AppState) => ({
8055
distributedSetup: state.system.distributedSetup,
8156
});

0 commit comments

Comments
 (0)