Updated design of object details panel (#1637)

- Removed old references to object details
- Created new Tags edit module

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2022-02-28 23:30:13 -07:00
committed by GitHub
parent 96d59fb7cc
commit 69a3ee6c1a
22 changed files with 1195 additions and 1680 deletions

View File

@@ -37,6 +37,9 @@ type BucketObject struct {
// content type
ContentType string `json:"content_type,omitempty"`
// etag
Etag string `json:"etag,omitempty"`
// expiration
Expiration string `json:"expiration,omitempty"`

View File

@@ -0,0 +1,35 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import * as React from "react";
import { SVGProps } from "react";
const LegalHoldIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
className={`min-icon`}
fill={"currentcolor"}
viewBox="0 0 256 256"
{...props}
>
<path
d="M253.46,219.34a17.76,17.76,0,0,1-5.37,13L232.57,248a18.57,18.57,0,0,1-13.19,5.38,17.74,17.74,0,0,1-13-5.38l-52.61-52.77a17.23,17.23,0,0,1-5.5-13.05,19.26,19.26,0,0,1,6.27-13.93L117.34,131.2,99.08,149.45a7,7,0,0,1-9.85,0l1.82,1.74a16.14,16.14,0,0,1,1.82,1.88,16.44,16.44,0,0,0,1.44,1.67,7.38,7.38,0,0,1,1.45,2c.19.49.48,1.14.87,2a9.89,9.89,0,0,1,.8,2.41,14.26,14.26,0,0,1-3.85,12.55q-.43.44-2.4,2.61t-2.76,3q-.8.79-2.7,2.4a16.88,16.88,0,0,1-3.2,2.24,28.58,28.58,0,0,1-3.2,1.3,11.22,11.22,0,0,1-3.76.65,13.45,13.45,0,0,1-9.85-4.06L6.6,122.42a13.43,13.43,0,0,1-4.06-9.85,11.4,11.4,0,0,1,.75-3.7,27,27,0,0,1,1.21-3.18,17.84,17.84,0,0,1,2.24-3.2c1.06-1.25,1.86-2.15,2.41-2.68s1.53-1.45,3-2.76l2.61-2.38a14.26,14.26,0,0,1,12.55-3.85,9.68,9.68,0,0,1,2.4.8l2,.87a7.33,7.33,0,0,1,2,1.45,20.77,20.77,0,0,0,1.67,1.44,19.1,19.1,0,0,1,1.89,1.82L38.9,99a7,7,0,0,1,0-9.85L89.21,38.78a7,7,0,0,1,9.85,0L97.24,37a13.64,13.64,0,0,1-1.8-1.92A11,11,0,0,0,94,33.44a6,6,0,0,1-1.44-2,20.39,20.39,0,0,0-.88-2,8.81,8.81,0,0,1-.8-2.4,17.58,17.58,0,0,1-.23-2.61,14.07,14.07,0,0,1,4.06-9.85c.29-.3,1.1-1.17,2.41-2.62s2.23-2.43,2.76-2.95,1.42-1.33,2.67-2.4a16.88,16.88,0,0,1,3.2-2.24,27.73,27.73,0,0,1,3.18-1.21,11.22,11.22,0,0,1,3.76-.65,13.48,13.48,0,0,1,9.79,4L181.7,65.67a13.39,13.39,0,0,1,4.05,9.85,11.22,11.22,0,0,1-.65,3.76,26.74,26.74,0,0,1-1.29,3.2,16.88,16.88,0,0,1-2.24,3.2q-1.59,1.88-2.4,2.67t-3,2.7l-2.62,2.41A14.24,14.24,0,0,1,161,97.3a10.31,10.31,0,0,1-2.41-.79l-1.86-.84a7.3,7.3,0,0,1-2-1.44,19.31,19.31,0,0,0-1.68-1.44A18,18,0,0,1,151.25,91l-1.73-1.82a7,7,0,0,1,0,9.85l-18.28,18.27,37.12,37.12a19.24,19.24,0,0,1,13.92-6.27,18.53,18.53,0,0,1,13.2,5.37l52.61,52.57a18.59,18.59,0,0,1,5.37,13.19Z"
transform="translate(-2.54 -2.58)"
/>
</svg>
);
export default LegalHoldIcon;

View File

@@ -0,0 +1,39 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import * as React from "react";
import { SVGProps } from "react";
const MetadataIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
className={`min-icon`}
fill={"currentcolor"}
viewBox="0 0 256 256"
{...props}
>
<path
d="M234.64,2.55H64.58a9,9,0,0,0-8.95,8.94V92h44.75a9,9,0,0,1,8.94,8.94v125.3a9,9,0,0,1-8.94,8.95H55.63v8.94a9,9,0,0,0,8.95,8.94H234.64a9,9,0,0,0,9-8.94V11.49A9,9,0,0,0,234.64,2.55ZM198.78,208.4H136.13a9,9,0,1,1,0-17.9h62.65a9,9,0,0,1,0,17.9Zm0-35.8H136.13a9,9,0,0,1,0-17.9h62.65a8.95,8.95,0,0,1,0,17.9Zm0-35.8H136.13a9,9,0,1,1,0-17.9h62.65a9,9,0,0,1,0,17.9Zm0-35.8H136.13a9,9,0,1,1,0-17.9h62.65a9,9,0,0,1,0,17.9Zm0-35.81H100.33a8.95,8.95,0,0,1,0-17.9h98.45a8.95,8.95,0,0,1,0,17.9Z"
transform="translate(-10.89 -2.55)"
/>
<path
d="M91.43,101H19.83a9,9,0,0,0-8.94,8.94v107.4a9,9,0,0,0,8.94,8.94h71.6a9,9,0,0,0,8.95-8.94V109.94A9,9,0,0,0,91.43,101Zm-17.9,98.44H37.73a8.95,8.95,0,1,1,0-17.9h35.8a8.95,8.95,0,0,1,0,17.9Zm0-26.84H37.73a8.95,8.95,0,1,1,0-17.9h35.8a8.95,8.95,0,0,1,0,17.9Zm0-26.85H37.73a8.95,8.95,0,1,1,0-17.9h35.8a8.95,8.95,0,0,1,0,17.9Z"
transform="translate(-10.89 -2.55)"
/>
</svg>
);
export default MetadataIcon;

View File

@@ -0,0 +1,35 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import * as React from "react";
import { SVGProps } from "react";
const ObjectInfoIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
className={`min-icon`}
fill={"currentcolor"}
viewBox="0 0 256 256"
{...props}
>
<path
d="M128,3.14C58.12,3.14,1.46,59,1.46,128S58.12,252.86,128,252.86,254.54,197,254.54,128h0C254.48,59.07,197.86,3.2,128,3.14M84.46,204.56a36.93,36.93,0,0,1-37.09-36.65h0c0-20.24,16.63-36.65,37.14-36.65s37.14,16.41,37.14,36.65S105,204.56,84.51,204.56h0M100,122.67a13,13,0,0,1-13.11-12.9,12.77,12.77,0,0,1,1.76-6.48l26.52-45.38a13.18,13.18,0,0,1,17.88-4.74,13,13,0,0,1,4.8,4.74l26.55,45.38a12.83,12.83,0,0,1-4.78,17.65,13.14,13.14,0,0,1-6.57,1.73ZM208.74,185a17.12,17.12,0,0,1-17.24,17H154.22A17.12,17.12,0,0,1,137,185V148.24a17.11,17.11,0,0,1,17.21-17h37.22a17.12,17.12,0,0,1,17.25,17v0Z"
transform="translate(-1.46 -3.14)"
/>
</svg>
);
export default ObjectInfoIcon;

View File

@@ -0,0 +1,39 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import * as React from "react";
import { SVGProps } from "react";
const RetentionIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
className={`min-icon`}
fill={"currentcolor"}
viewBox="0 0 256 256"
{...props}
>
<path
d="M222.54,17.88h-24.4V14.76a12.2,12.2,0,1,0-24.4,0V17.9H78.93V14.76a12.21,12.21,0,1,0-24.41,0V17.9H33.42a30.46,30.46,0,0,0-30.88,30V223.47a30.54,30.54,0,0,0,30.88,30H222.56a30.47,30.47,0,0,0,30.86-29.94V47.9a30.53,30.53,0,0,0-30.88-30M26.94,47.79a6.27,6.27,0,0,1,6.45-6.08H54.52v3.34a12.21,12.21,0,0,0,24.39,0V41.71h94.81v3.34a12.2,12.2,0,0,0,24.4,0V41.71h24.4A6.28,6.28,0,0,1,229,47.77h0v26h-202ZM229.14,223.4a6.5,6.5,0,0,1-6.6,6.09H33.42A6.27,6.27,0,0,1,27,223.42h0V97.55H229.14Z"
transform="translate(-2.54 -2.55)"
/>
<path
d="M96.62,195.15,128,200.61l31.36-5.46a16,16,0,0,0,16.41-15.05V148.49a16.05,16.05,0,0,0-16.85-15.05H148.22v-9.93a20.35,20.35,0,0,0-40.42,0v9.93H97.08a16.05,16.05,0,0,0-16.85,15.05v31.63a16,16,0,0,0,16.41,15M132,166.22v5.72a3.76,3.76,0,0,1-3.76,3.77h-.46a3.76,3.76,0,0,1-3.76-3.77h0v-5.72a7.13,7.13,0,1,1,9.9-1.92,7,7,0,0,1-1.92,1.92m-15.82-42.69a11.91,11.91,0,0,1,23.66,0v9.93H116.17Z"
transform="translate(-2.54 -2.55)"
/>
</svg>
);
export default RetentionIcon;

View File

@@ -0,0 +1,35 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import * as React from "react";
import { SVGProps } from "react";
const TagsIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
className={`min-icon`}
fill={"currentcolor"}
viewBox="0 0 256 256"
{...props}
>
<path
d="M8.18,94.43V21.24A20.26,20.26,0,0,1,27.69,1.74h73.19A51,51,0,0,1,134.25,15.6L242.6,136.2a21,21,0,0,1,0,27.73l-84.8,84.81a20.17,20.17,0,0,1-27.74,0L22.05,127.8A55.46,55.46,0,0,1,8.18,94.43ZM39.94,52.24a19.31,19.31,0,0,0,18.7,18.94A19.42,19.42,0,0,0,77.58,52.24,19.29,19.29,0,0,0,58.64,33.53,19.17,19.17,0,0,0,39.94,52.24Z"
transform="translate(-8.18 -1.74)"
/>
</svg>
);
export default TagsIcon;

View File

@@ -169,3 +169,8 @@ export { default as LockIcon } from "./LockIcon";
export { default as BackCaretIcon } from "./BackCaretIcon";
export { default as VersionsIcon } from "./VersionsIcon";
export { default as NewPathIcon } from "./NewPathIcon";
export { default as ObjectInfoIcon } from "./ObjectInfoIcon";
export { default as MetadataIcon } from "./MetadataIcon";
export { default as LegalHoldIcon } from "./LegalHoldIcon";
export { default as RetentionIcon } from "./RetentionIcon";
export { default as TagsIcon } from "./TagsIcon";

View File

@@ -33,25 +33,25 @@ export interface MultiSelectionItem {
tooltip: string;
}
interface IMultiSelectionPanelProps {
interface IActionsListSectionProps {
items: MultiSelectionItem[];
title: string;
title: string | React.ReactNode;
classes: any;
}
const MultiSelectionPanel = ({
const ActionsListSection = ({
items,
classes,
title,
}: IMultiSelectionPanelProps) => {
}: IActionsListSectionProps) => {
return (
<Fragment>
<div className={classes.titleLabel}>{title}</div>
<ul className={classes.objectActions}>
<li>Actions:</li>
{items.map((actionItem) => {
{items.map((actionItem, index) => {
return (
<li>
<li key={`action-element-${index.toString()}`}>
<ObjectActionButton
label={actionItem.label}
icon={actionItem.icon}
@@ -66,4 +66,4 @@ const MultiSelectionPanel = ({
);
};
export default withStyles(styles)(MultiSelectionPanel);
export default withStyles(styles)(ActionsListSection);

View File

@@ -98,7 +98,7 @@ import UploadFilesButton from "../../UploadFilesButton";
import DetailsListPanel from "./DetailsListPanel";
import ObjectDetailPanel from "./ObjectDetailPanel";
import RBIconButton from "../../../BucketDetails/SummaryItems/RBIconButton";
import MultiSelectionPanel from "./MultiSelectionPanel";
import ActionsListSection from "./ActionsListSection";
import { listModeColumns, rewindModeColumns } from "./ListObjectsHelpers";
import VersionsNavigator from "../ObjectDetails/VersionsNavigator";
@@ -1007,6 +1007,8 @@ const ListObjects = ({
}
const selectAllItems = () => {
setSelectedInternalPaths(null);
if (selectedObjects.length === payload.length) {
setSelectedObjects([]);
return;
@@ -1312,7 +1314,7 @@ const ListObjects = ({
}}
>
{selectedObjects.length > 0 && (
<MultiSelectionPanel
<ActionsListSection
items={multiActionButtons}
title={"Selected Objects:"}
/>

View File

@@ -16,7 +16,7 @@
import React, { Fragment, useEffect, useState } from "react";
import { connect } from "react-redux";
import { Box, LinearProgress } from "@mui/material";
import { Box, Button, LinearProgress } from "@mui/material";
import { withStyles } from "@mui/styles";
import createStyles from "@mui/styles/createStyles";
import get from "lodash/get";
@@ -29,7 +29,7 @@ import {
detailsPanel,
} from "../../../../Common/FormComponents/common/styleLibrary";
import { IFileInfo } from "../ObjectDetails/types";
import { download } from "../utils";
import { download, extensionPreview } from "../utils";
import { ErrorResponseHandler } from "../../../../../../common/types";
import {
setErrorSnackMessage,
@@ -38,7 +38,9 @@ import {
import {
decodeFileName,
encodeFileName,
niceBytes,
niceBytesInt,
niceDaysInt,
} from "../../../../../../common/utils";
import { IAM_SCOPES } from "../../../../../../common/SecureComponent/permissions";
import {
@@ -49,8 +51,12 @@ import {
} from "../../../../ObjectBrowser/actions";
import { AppState } from "../../../../../../store";
import {
DisabledIcon,
LegalHoldIcon,
MetadataIcon,
ObjectInfoIcon,
PreviewIcon,
RetentionIcon,
TagsIcon,
VersionsIcon,
} from "../../../../../../icons";
import { ShareIcon, DownloadIcon, DeleteIcon } from "../../../../../../icons";
@@ -59,18 +65,17 @@ import api from "../../../../../../common/api";
import ShareFile from "../ObjectDetails/ShareFile";
import SetRetention from "../ObjectDetails/SetRetention";
import DeleteObject from "../ListObjects/DeleteObject";
import AddTagModal from "../ObjectDetails/AddTagModal";
import DeleteTagModal from "../ObjectDetails/DeleteTagModal";
import SetLegalHoldModal from "../ObjectDetails/SetLegalHoldModal";
import RestoreFileVersion from "../ObjectDetails/RestoreFileVersion";
import { SecureComponent } from "../../../../../../common/SecureComponent";
import ObjectTags from "../ObjectDetails/ObjectTags";
import LabelWithIcon from "../../../BucketDetails/SummaryItems/LabelWithIcon";
import {
hasPermission,
SecureComponent,
} from "../../../../../../common/SecureComponent";
import PreviewFileModal from "../Preview/PreviewFileModal";
import ObjectActionButton from "./ObjectActionButton";
import ObjectMetaData from "../ObjectDetails/ObjectMetaData";
import EditablePropertyItem from "../../../BucketDetails/SummaryItems/EditablePropertyItem";
import LabelValuePair from "../../../../Common/UsageBarWrapper/LabelValuePair";
import ActionsListSection from "./ActionsListSection";
import { displayFileIconName } from "./utils";
import TagsModal from "../ObjectDetails/TagsModal";
const styles = () =>
createStyles({
@@ -86,6 +91,31 @@ const styles = () =>
width: 10,
},
},
ObjectDetailsTitle: {
display: "flex",
alignItems: "center",
},
objectNameContainer: {
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
alignItems: "center",
marginLeft: 10,
},
headerForSection: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
paddingBottom: 15,
borderBottom: "#E2E2E2 2px solid",
fontWeight: "bold",
fontSize: 18,
color: "#000",
margin: "20px 22px",
},
capitalizeFirst: {
textTransform: "capitalize",
},
"@global": {
".progressDetails": {
paddingTop: 3,
@@ -154,8 +184,6 @@ const ObjectDetailPanel = ({
const [shareFileModalOpen, setShareFileModalOpen] = useState<boolean>(false);
const [retentionModalOpen, setRetentionModalOpen] = useState<boolean>(false);
const [tagModalOpen, setTagModalOpen] = useState<boolean>(false);
const [deleteTagModalOpen, setDeleteTagModalOpen] = useState<boolean>(false);
const [selectedTag, setSelectedTag] = useState<string[]>(["", ""]);
const [legalholdOpen, setLegalholdOpen] = useState<boolean>(false);
const [actualInfo, setActualInfo] = useState<IFileInfo | null>(null);
const [allInfoElements, setAllInfoElements] = useState<IFileInfo[]>([]);
@@ -271,11 +299,6 @@ const ObjectDetailPanel = ({
setShareFileModalOpen(false);
};
const deleteTag = (tagKey: string, tagLabel: string) => {
setSelectedTag([tagKey, tagLabel]);
setDeleteTagModalOpen(true);
};
const downloadObject = (object: IFileInfo) => {
const identityDownload = encodeFileName(
`${bucketName}-${object.name}-${new Date().getTime()}-${Math.random()}`
@@ -332,13 +355,6 @@ const ObjectDetailPanel = ({
}
};
const closeDeleteTagModal = (reloadObjectData: boolean) => {
setDeleteTagModalOpen(false);
if (reloadObjectData) {
setLoadObjectData(true);
}
};
const closeRestoreModal = (reloadObjectData: boolean) => {
setRestoreVersionOpen(false);
setRestoreVersion("");
@@ -361,6 +377,121 @@ const ObjectDetailPanel = ({
? objectNameArray[objectNameArray.length - 1]
: actualInfo.name;
const multiActionButtons = [
{
action: () => {
downloadObject(actualInfo);
},
label: "Download",
disabled: !!actualInfo.is_delete_marker,
icon: <DownloadIcon />,
tooltip: "Download this Object",
},
{
action: () => {
shareObject();
},
label: "Share",
disabled: !!actualInfo.is_delete_marker,
icon: <ShareIcon />,
tooltip: "Share this File",
},
{
action: () => {
setPreviewOpen(true);
},
label: "Preview",
disabled:
!!actualInfo.is_delete_marker ||
extensionPreview(currentItem) === "none",
icon: <PreviewIcon />,
tooltip: "Preview this File",
},
{
action: () => {
setLegalholdOpen(true);
},
label: "Legal Hold",
disabled:
!!actualInfo.is_delete_marker ||
extensionPreview(currentItem) === "none" ||
!hasPermission(bucketName, [IAM_SCOPES.S3_PUT_OBJECT_LEGAL_HOLD]) ||
selectedVersion !== "",
icon: <LegalHoldIcon />,
tooltip: "Change Legal Hold rules for this File",
},
{
action: openRetentionModal,
label: "Retention",
disabled:
!!actualInfo.is_delete_marker ||
extensionPreview(currentItem) === "none" ||
!hasPermission(bucketName, [IAM_SCOPES.S3_GET_OBJECT_RETENTION]) ||
selectedVersion !== "",
icon: <RetentionIcon />,
tooltip: "Change Retention rules for this File",
},
{
action: () => {
setTagModalOpen(true);
},
label: "Tags",
disabled:
!!actualInfo.is_delete_marker ||
extensionPreview(currentItem) === "none" ||
selectedVersion !== "",
icon: <TagsIcon />,
tooltip: "Change Tags for this File",
},
{
action: () => {
setVersionsModeEnabled(!versionsMode, objectName);
},
label: versionsMode ? "Hide Object Versions" : "Display Object Versions",
icon: <VersionsIcon />,
disabled: !(actualInfo.version_id && actualInfo.version_id !== "null"),
tooltip: "Display Versions for this file",
},
];
/*
*
*
* <Box className={classes.detailContainer}>
{selectedVersion === "" ? (
<LabelValuePair
label={"Tags:"}
value={
<ObjectTags
objectInfo={actualInfo}
tagKeys={tagKeys}
bucketName={bucketName}
onDeleteTag={deleteTag}
onAddTagClick={() => {
setTagModalOpen(true);
}}
/>
}
/>
) : (
<Fragment>
<strong>Tags: </strong>
<br />
</Fragment>
)}
</Box>
*
* */
const calculateLastModifyTime = (lastModified: string) => {
const currentTime = new Date();
const modifiedTime = new Date(lastModified);
const difTime = currentTime.getTime() - modifiedTime.getTime();
return `${niceDaysInt(difTime, "ms")} ago`;
};
return (
<Fragment>
{shareFileModalOpen && actualInfo && (
@@ -389,27 +520,6 @@ const ObjectDetailPanel = ({
versioning={distributedSetup}
/>
)}
{tagModalOpen && actualInfo && (
<AddTagModal
modalOpen={tagModalOpen}
currentTags={actualInfo.tags}
selectedObject={internalPaths}
versionId={actualInfo.version_id}
bucketName={bucketName}
onCloseAndUpdate={closeAddTagModal}
/>
)}
{deleteTagModalOpen && actualInfo && (
<DeleteTagModal
deleteOpen={deleteTagModalOpen}
currentTags={actualInfo.tags}
selectedObject={actualInfo.name}
versionId={actualInfo.version_id}
bucketName={bucketName}
onCloseAndUpdate={closeDeleteTagModal}
selectedTag={selectedTag}
/>
)}
{legalholdOpen && actualInfo && (
<SetLegalHoldModal
open={legalholdOpen}
@@ -442,6 +552,14 @@ const ObjectDetailPanel = ({
onClosePreview={closePreviewWindow}
/>
)}
{tagModalOpen && actualInfo && (
<TagsModal
modalOpen={tagModalOpen}
bucketName={bucketName}
actualInfo={actualInfo}
onCloseAndUpdate={closeAddTagModal}
/>
)}
{!actualInfo && (
<Grid item xs={12}>
@@ -449,76 +567,55 @@ const ObjectDetailPanel = ({
</Grid>
)}
<div className={classes.titleLabel}>{objectName}</div>
<ActionsListSection
title={
<div className={classes.ObjectDetailsTitle}>
{displayFileIconName(objectName, true)}
<span className={classes.objectNameContainer}>{objectName}</span>
</div>
}
items={multiActionButtons}
/>
<ul className={classes.objectActions}>
<li>Actions:</li>
<li>
<ObjectActionButton
label={"Download"}
icon={<DownloadIcon />}
onClick={() => {
downloadObject(actualInfo);
}}
disabled={actualInfo.is_delete_marker}
/>
</li>
<li>
<ObjectActionButton
label={"Share"}
icon={<ShareIcon />}
onClick={() => {
shareObject();
}}
disabled={actualInfo.is_delete_marker}
/>
</li>
<li>
<ObjectActionButton
label={"Preview"}
icon={<PreviewIcon />}
onClick={() => {
setPreviewOpen(true);
}}
disabled={actualInfo.is_delete_marker}
/>
</li>
<SecureComponent
scopes={[IAM_SCOPES.S3_DELETE_OBJECT]}
resource={bucketName}
matchAll
errorProps={{ disabled: true }}
>
<li>
<ObjectActionButton
label={"Delete"}
icon={<DeleteIcon />}
<Grid item xs={12} sx={{ textAlign: "center" }}>
{selectedVersion === "" && (
<SecureComponent
resource={bucketName}
scopes={[IAM_SCOPES.S3_DELETE_OBJECT]}
matchAll
errorProps={{ disabled: true }}
>
<Button
startIcon={<DeleteIcon />}
color="secondary"
variant={"outlined"}
onClick={() => {
setDeleteOpen(true);
}}
disabled={actualInfo.is_delete_marker || selectedVersion !== ""}
/>
</li>
</SecureComponent>
<li>
<ObjectActionButton
label={
versionsMode ? "Hide Object Versions" : "Display Object Versions"
}
icon={<VersionsIcon />}
onClick={() => {
setVersionsModeEnabled(!versionsMode, objectName);
}}
disabled={
!(actualInfo.version_id && actualInfo.version_id !== "null")
}
/>
</li>
</ul>
<div className={classes.actionsTray}>
<h1 className={classes.sectionTitle}>Details</h1>
</div>
sx={{
width: "calc(100% - 44px)",
margin: "8px 0",
"& svg.min-icon": {
width: 14,
height: 14,
},
}}
>
Delete
</Button>
</SecureComponent>
)}
</Grid>
<Grid item xs={12} className={classes.headerForSection}>
<span>Object Info</span>
<ObjectInfoIcon />
</Grid>
<Box className={classes.detailContainer}>
<strong>Name:</strong>
<br />
{objectName}
</Box>
{selectedVersion !== "" && (
<Box className={classes.detailContainer}>
<strong>Version ID:</strong>
@@ -527,91 +624,56 @@ const ObjectDetailPanel = ({
</Box>
)}
<Box className={classes.detailContainer}>
{selectedVersion === "" ? (
<LabelValuePair
label={"Tags:"}
value={
<ObjectTags
objectInfo={actualInfo}
tagKeys={tagKeys}
bucketName={bucketName}
onDeleteTag={deleteTag}
onAddTagClick={() => {
setTagModalOpen(true);
}}
/>
}
/>
) : (
<Fragment>
<strong>Tags: </strong>
<strong>Size:</strong>
<br />
{niceBytes(actualInfo.size || "0")}
</Box>
{actualInfo.version_id &&
actualInfo.version_id !== "null" &&
selectedVersion === "" && (
<Box className={classes.detailContainer}>
<strong>Versions:</strong>
<br />
{tagKeys.length === 0
? "N/A"
: tagKeys.map((tagKey, index) => {
return (
<span key={`key-vs-${index.toString()}`}>
{tagKey}:{get(actualInfo, `tags.${tagKey}`, "")}
{index < tagKeys.length - 1 ? ", " : ""}
</span>
);
})}
</Fragment>
{versions.length} version{versions.length !== 1 ? "s" : ""},{" "}
{niceBytesInt(totalVersionsSize)}
</Box>
)}
{selectedVersion === "" && (
<Box className={classes.detailContainer}>
<strong>Last Modified:</strong>
<br />
{calculateLastModifyTime(actualInfo.last_modified)}
</Box>
)}
<Box className={classes.detailContainer}>
<strong>ETAG:</strong>
<br />
{actualInfo.etag || "N/A"}
</Box>
<Box className={classes.detailContainer}>
<strong>Tags:</strong>
<br />
{tagKeys.length === 0
? "N/A"
: tagKeys.map((tagKey, index) => {
return (
<span key={`key-vs-${index.toString()}`}>
{tagKey}:{get(actualInfo, `tags.${tagKey}`, "")}
{index < tagKeys.length - 1 ? ", " : ""}
</span>
);
})}
</Box>
<Box className={classes.detailContainer}>
<SecureComponent
scopes={[IAM_SCOPES.S3_GET_OBJECT_LEGAL_HOLD]}
resource={bucketName}
>
{selectedVersion === "" ? (
<LabelValuePair
label={""}
value={
actualInfo.version_id && actualInfo.version_id !== "null" ? (
<EditablePropertyItem
iamScopes={[IAM_SCOPES.S3_PUT_OBJECT_LEGAL_HOLD]}
secureCmpProps={{
matchAll: false,
errorProps: {
disabled: true,
onClick: null,
},
}}
resourceName={bucketName}
property={"Legal Hold:"}
value={
actualInfo.legal_hold_status
? actualInfo.legal_hold_status.toLowerCase()
: "Off"
}
onEdit={() => {
setLegalholdOpen(true);
}}
isLoading={false}
/>
) : (
<LabelValuePair
label={"Legal Hold:"}
value={
<LabelWithIcon
icon={<DisabledIcon />}
label={
<label className={classes.textMuted}>Disabled</label>
}
/>
}
/>
)
}
/>
) : (
<Fragment>
<strong>Legal Hold:</strong>
<br />
{actualInfo.legal_hold_status ? "On" : "Off"}
</Fragment>
)}
<Fragment>
<strong>Legal Hold:</strong>
<br />
{actualInfo.legal_hold_status ? "On" : "Off"}
</Fragment>
</SecureComponent>
</Box>
<Box className={classes.detailContainer}>
@@ -619,56 +681,31 @@ const ObjectDetailPanel = ({
scopes={[IAM_SCOPES.S3_GET_OBJECT_RETENTION]}
resource={bucketName}
>
{selectedVersion === "" ? (
<LabelValuePair
label={""}
value={
actualInfo.version_id && actualInfo.version_id !== "null" ? (
<EditablePropertyItem
iamScopes={[IAM_SCOPES.S3_PUT_OBJECT_RETENTION]}
secureCmpProps={{
matchAll: false,
}}
resourceName={bucketName}
property={"Retention:"}
value={
actualInfo.retention_mode
? actualInfo.retention_mode.toLowerCase()
: "None"
}
onEdit={openRetentionModal}
isLoading={false}
/>
) : (
<LabelValuePair
label={"Retention:"}
value={
<LabelWithIcon
icon={<DisabledIcon />}
label={
<label className={classes.textMuted}>Disabled</label>
}
/>
}
/>
)
}
/>
) : (
<Fragment>
<strong>Object Retention:</strong>
<br />
{actualInfo.retention_mode
? actualInfo.retention_mode.toLowerCase()
: "None"}
</Fragment>
)}
<Fragment>
<strong>Retention Policy:</strong>
<br />
<span className={classes.capitalizeFirst}>
{actualInfo.version_id && actualInfo.version_id !== "null" ? (
<Fragment>
{actualInfo.retention_mode
? actualInfo.retention_mode.toLowerCase()
: "None"}
</Fragment>
) : (
<Fragment>
{actualInfo.retention_mode
? actualInfo.retention_mode.toLowerCase()
: "None"}
</Fragment>
)}
</span>
</Fragment>
</SecureComponent>
</Box>
<hr className={classes.hrClass} />
<div className={classes.actionsTray}>
<h1 className={classes.sectionTitle}>Object Metadata</h1>
</div>
<Grid item xs={12} className={classes.headerForSection}>
<span>Metadata</span>
<MetadataIcon />
</Grid>
<Box className={classes.detailContainer}>
{actualInfo ? (
<ObjectMetaData
@@ -679,29 +716,6 @@ const ObjectDetailPanel = ({
/>
) : null}
</Box>
<hr className={classes.hrClass} />
{actualInfo.version_id &&
actualInfo.version_id !== "null" &&
selectedVersion === "" && (
<Fragment>
<div className={classes.actionsTray}>
<h1 className={classes.sectionTitle}>Versions</h1>
</div>
<Box className={classes.detailContainer}>
<Box className={classes.metadataLinear}>
<strong>Total available versions</strong>
<br />
{versions.length}
</Box>
<Box className={classes.metadataLinear}>
<strong>Versions Stored size:</strong>
<br />
{niceBytesInt(totalVersionsSize)}
</Box>
</Box>
</Fragment>
)}
</Fragment>
);
};

View File

@@ -17,6 +17,7 @@
export interface BucketObject {
name: string;
size: number;
etag?: string;
last_modified: Date;
content_type: string;
version_id: string;

View File

@@ -1,181 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useState } from "react";
import get from "lodash/get";
import { connect } from "react-redux";
import { Button, Grid } from "@mui/material";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { setModalErrorSnackMessage } from "../../../../../../actions";
import { AppState } from "../../../../../../store";
import { ErrorResponseHandler } from "../../../../../../common/types";
import InputBoxWrapper from "../../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import ModalWrapper from "../../../../Common/ModalWrapper/ModalWrapper";
import api from "../../../../../../common/api";
import { decodeFileName } from "../../../../../../common/utils";
import {
formFieldStyles,
modalStyleUtils,
spacingUtils,
} from "../../../../Common/FormComponents/common/styleLibrary";
import { AddNewTagIcon } from "../../../../../../icons";
interface ITagModal {
modalOpen: boolean;
currentTags: any;
bucketName: string;
versionId: string | null;
onCloseAndUpdate: (refresh: boolean) => void;
selectedObject: string;
distributedSetup: boolean;
setModalErrorSnackMessage: typeof setModalErrorSnackMessage;
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
pathLabel: {
marginTop: 0,
marginBottom: 32,
},
...formFieldStyles,
...modalStyleUtils,
...spacingUtils,
});
const AddTagModal = ({
modalOpen,
currentTags,
selectedObject,
onCloseAndUpdate,
bucketName,
versionId,
distributedSetup,
setModalErrorSnackMessage,
classes,
}: ITagModal) => {
const [newKey, setNewKey] = useState<string>("");
const [newLabel, setNewLabel] = useState<string>("");
const [isSending, setIsSending] = useState<boolean>(false);
const resetForm = () => {
setNewLabel("");
setNewKey("");
};
const addTagProcess = () => {
setIsSending(true);
const newTag: any = {};
newTag[newKey] = newLabel;
const newTagList = { ...currentTags, ...newTag };
const verID = distributedSetup ? versionId : "null";
api
.invoke(
"PUT",
`/api/v1/buckets/${bucketName}/objects/tags?prefix=${selectedObject}&version_id=${verID}`,
{ tags: newTagList }
)
.then((res: any) => {
onCloseAndUpdate(true);
setIsSending(false);
})
.catch((error: ErrorResponseHandler) => {
setModalErrorSnackMessage(error);
setIsSending(false);
});
};
return (
<React.Fragment>
<ModalWrapper
modalOpen={modalOpen}
title="Add New Tag to the Object"
onClose={() => {
onCloseAndUpdate(true);
}}
titleIcon={<AddNewTagIcon />}
>
<Grid container>
<div className={classes.spacerBottom}>
<strong>Selected Object</strong>: {decodeFileName(selectedObject)}
</div>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
value={newKey}
label={"Tag Key"}
id={"newTagKey"}
name={"newTagKey"}
placeholder={"Enter Tag Key"}
onChange={(e) => {
setNewKey(e.target.value);
}}
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
value={newLabel}
label={"Tag Label"}
id={"newTagLabel"}
name={"newTagLabel"}
placeholder={"Enter Tag Label"}
onChange={(e) => {
setNewLabel(e.target.value);
}}
/>
</Grid>
<Grid item xs={12} className={classes.modalButtonBar}>
<Button
type="button"
variant="outlined"
color="primary"
onClick={resetForm}
>
Clear
</Button>
<Button
type="submit"
variant="contained"
color="primary"
disabled={
newLabel.trim() === "" || newKey.trim() === "" || isSending
}
onClick={addTagProcess}
>
Save
</Button>
</Grid>
</Grid>
</ModalWrapper>
</React.Fragment>
);
};
const mapStateToProps = ({ system }: AppState) => ({
distributedSetup: get(system, "distributedSetup", false),
});
const mapDispatchToProps = {
setModalErrorSnackMessage,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
export default withStyles(styles)(connector(AddTagModal));

View File

@@ -1,118 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import get from "lodash/get";
import { connect } from "react-redux";
import { DialogContentText } from "@mui/material";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { setErrorSnackMessage } from "../../../../../../actions";
import { AppState } from "../../../../../../store";
import { ErrorResponseHandler } from "../../../../../../common/types";
import { encodeFileName } from "../../../../../../common/utils";
import useApi from "../../../../Common/Hooks/useApi";
import ConfirmDialog from "../../../../Common/ModalWrapper/ConfirmDialog";
import { ConfirmDeleteIcon } from "../../../../../../icons";
interface IDeleteTagModal {
deleteOpen: boolean;
currentTags: any;
bucketName: string;
versionId: string | null;
selectedTag: string[];
onCloseAndUpdate: (refresh: boolean) => void;
selectedObject: string;
distributedSetup: boolean;
setErrorSnackMessage: typeof setErrorSnackMessage;
classes: any;
}
const styles = (theme: Theme) => createStyles({});
const DeleteTagModal = ({
deleteOpen,
currentTags,
selectedObject,
selectedTag,
onCloseAndUpdate,
bucketName,
versionId,
distributedSetup,
setErrorSnackMessage,
classes,
}: IDeleteTagModal) => {
const [tagKey, tagLabel] = selectedTag;
const onDelSuccess = () => onCloseAndUpdate(true);
const onDelError = (err: ErrorResponseHandler) => setErrorSnackMessage(err);
const onClose = () => onCloseAndUpdate(false);
const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError);
if (!selectedTag) {
return null;
}
const onConfirmDelete = () => {
const cleanObject = { ...currentTags };
delete cleanObject[tagKey];
const verID = distributedSetup ? versionId : "null";
invokeDeleteApi(
"PUT",
`/api/v1/buckets/${bucketName}/objects/tags?prefix=${encodeFileName(
selectedObject
)}&version_id=${verID}`,
{ tags: cleanObject }
);
};
return (
<ConfirmDialog
title={`Delete Tag`}
confirmText={"Delete"}
isOpen={deleteOpen}
titleIcon={<ConfirmDeleteIcon />}
isLoading={deleteLoading}
onConfirm={onConfirmDelete}
onClose={onClose}
confirmationContent={
<DialogContentText>
Are you sure you want to delete the tag{" "}
<b className={classes.wrapText}>
{tagKey} : {tagLabel}
</b>{" "}
from {selectedObject}?
</DialogContentText>
}
/>
);
};
const mapStateToProps = ({ system }: AppState) => ({
distributedSetup: get(system, "distributedSetup", false),
});
const mapDispatchToProps = {
setErrorSnackMessage,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
export default withStyles(styles)(connector(DeleteTagModal));

View File

@@ -1,843 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment, useEffect, useState } from "react";
import get from "lodash/get";
import * as reactMoment from "react-moment";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import { Box, CircularProgress, LinearProgress } from "@mui/material";
import Grid from "@mui/material/Grid";
import ShareFile from "./ShareFile";
import {
actionsTray,
buttonsStyles,
containerForHeader,
hrClass,
tableStyles,
spacingUtils,
textStyleUtils,
} from "../../../../Common/FormComponents/common/styleLibrary";
import { IFileInfo } from "./types";
import { download, extensionPreview } from "../utils";
import history from "../../../../../../history";
import api from "../../../../../../common/api";
import TableWrapper, {
ItemActions,
} from "../../../../Common/TableWrapper/TableWrapper";
import { ErrorResponseHandler } from "../../../../../../common/types";
import {
setErrorSnackMessage,
setSnackBarMessage,
} from "../../../../../../actions";
import { decodeFileName, encodeFileName } from "../../../../../../common/utils";
import { IAM_SCOPES } from "../../../../../../common/SecureComponent/permissions";
import SetRetention from "./SetRetention";
import DeleteObject from "../ListObjects/DeleteObject";
import AddTagModal from "./AddTagModal";
import DeleteTagModal from "./DeleteTagModal";
import SetLegalHoldModal from "./SetLegalHoldModal";
import ScreenTitle from "../../../../Common/ScreenTitle/ScreenTitle";
import PreviewFileContent from "../Preview/PreviewFileContent";
import RestoreFileVersion from "./RestoreFileVersion";
import PageLayout from "../../../../Common/Layout/PageLayout";
import VerticalTabs from "../../../../Common/VerticalTabs/VerticalTabs";
import { SecureComponent } from "../../../../../../common/SecureComponent";
import {
completeObject,
setNewObject,
updateProgress,
} from "../../../../ObjectBrowser/actions";
import RBIconButton from "../../../BucketDetails/SummaryItems/RBIconButton";
import SearchBox from "../../../../Common/SearchBox";
import ObjectTags from "./ObjectTags";
import { AppState } from "../../../../../../store";
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import { withStyles } from "@mui/styles";
import { DisabledIcon } from "../../../../../../icons";
import LabelWithIcon from "../../../BucketDetails/SummaryItems/LabelWithIcon";
const RecoverIcon = React.lazy(
() => import("../../../../../../icons/RecoverIcon")
);
const ShareIcon = React.lazy(() => import("../../../../../../icons/ShareIcon"));
const DownloadIcon = React.lazy(
() => import("../../../../../../icons/DownloadIcon")
);
const DeleteIcon = React.lazy(
() => import("../../../../../../icons/DeleteIcon")
);
const ObjectBrowserIcon = React.lazy(
() => import("../../../../../../icons/ObjectBrowserIcon")
);
const ObjectMetaData = React.lazy(() => import("./ObjectMetaData"));
const EditablePropertyItem = React.lazy(
() => import("../../../BucketDetails/SummaryItems/EditablePropertyItem")
);
const LabelValuePair = React.lazy(
() => import("../../../../Common/UsageBarWrapper/LabelValuePair")
);
const styles = (theme: Theme) =>
createStyles({
propertiesIcon: {
marginLeft: 5,
"& .min-icon": {
height: 12,
},
},
tag: {
marginRight: 6,
fontSize: 10,
fontWeight: 700,
"&.MuiChip-sizeSmall": {
height: 18,
},
"& .min-icon": {
height: 10,
width: 10,
},
},
search: {
marginBottom: 8,
"&.MuiFormControl-root": {
marginRight: 0,
},
},
capitalizeFirst: {
textTransform: "capitalize",
"& .min-icon": {
width: 16,
height: 16,
},
},
titleCol: {
width: "25%",
},
titleItem: {
width: "35%",
},
"@global": {
".progressDetails": {
paddingTop: 3,
display: "inline-block",
position: "relative",
width: 18,
height: 18,
},
".progressDetails > .MuiCircularProgress-root": {
position: "absolute",
left: 0,
top: 3,
},
},
tabsContainer: {
border: "1px solid #eaeaea",
borderTop: 0,
},
...hrClass,
...buttonsStyles,
...actionsTray,
...tableStyles,
...spacingUtils,
...textStyleUtils,
...containerForHeader(theme.spacing(4)),
});
interface IObjectDetailsProps {
classes: any;
downloadingFiles: string[];
rewindEnabled: boolean;
rewindDate: any;
match: any;
bucketToRewind: string;
distributedSetup: boolean;
setErrorSnackMessage: typeof setErrorSnackMessage;
setSnackBarMessage: typeof setSnackBarMessage;
setNewObject: typeof setNewObject;
updateProgress: typeof updateProgress;
completeObject: typeof completeObject;
}
const emptyFile: IFileInfo = {
is_latest: true,
last_modified: "",
legal_hold_status: "",
name: "",
retention_mode: "",
retention_until_date: "",
size: "0",
tags: {},
version_id: null,
};
const twoColCssGridLayoutConfig = {
display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "2fr 1fr" },
gridAutoFlow: { xs: "dense", sm: "row" },
gap: 2,
};
const ObjectDetails = ({
classes,
downloadingFiles,
distributedSetup,
setErrorSnackMessage,
setNewObject,
updateProgress,
completeObject,
match,
}: IObjectDetailsProps) => {
const [loadObjectData, setLoadObjectData] = useState<boolean>(true);
const [shareFileModalOpen, setShareFileModalOpen] = useState<boolean>(false);
const [retentionModalOpen, setRetentionModalOpen] = useState<boolean>(false);
const [tagModalOpen, setTagModalOpen] = useState<boolean>(false);
const [deleteTagModalOpen, setDeleteTagModalOpen] = useState<boolean>(false);
const [selectedTag, setSelectedTag] = useState<string[]>(["", ""]);
const [legalholdOpen, setLegalholdOpen] = useState<boolean>(false);
const [actualInfo, setActualInfo] = useState<IFileInfo | null>(null);
const [objectToShare, setObjectToShare] = useState<IFileInfo | null>(null);
const [versions, setVersions] = useState<IFileInfo[]>([]);
const [filterVersion, setFilterVersion] = useState<string>("");
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [restoreVersionOpen, setRestoreVersionOpen] = useState<boolean>(false);
const [restoreVersion, setRestoreVersion] = useState<string>("");
const internalPaths = get(match.params, "subpaths", "");
const internalPathsDecoded = decodeFileName(internalPaths) || "";
const bucketName = match.params["bucketName"];
const allPathData = internalPathsDecoded.split("/");
const currentItem = allPathData.pop() || "";
// calculate object name to display
let objectNameArray: string[] = [];
if (actualInfo) {
objectNameArray = actualInfo.name.split("/");
}
useEffect(() => {
if (loadObjectData && internalPaths !== "") {
api
.invoke(
"GET",
`/api/v1/buckets/${bucketName}/objects?prefix=${internalPaths}${
distributedSetup ? "&with_versions=true" : ""
}`
)
.then((res: IFileInfo[]) => {
const result = get(res, "objects", []);
if (distributedSetup) {
setActualInfo(
result.find((el: IFileInfo) => el.is_latest) || emptyFile
);
setVersions(result);
} else {
setActualInfo(result[0]);
setVersions([]);
}
setLoadObjectData(false);
})
.catch((error: ErrorResponseHandler) => {
setErrorSnackMessage(error);
setLoadObjectData(false);
});
}
}, [
loadObjectData,
bucketName,
internalPaths,
setErrorSnackMessage,
distributedSetup,
]);
let tagKeys: string[] = [];
if (actualInfo && actualInfo.tags) {
tagKeys = Object.keys(actualInfo.tags);
}
const openRetentionModal = () => {
setRetentionModalOpen(true);
};
const closeRetentionModal = (updateInfo: boolean) => {
setRetentionModalOpen(false);
if (updateInfo) {
setLoadObjectData(true);
}
};
const shareObject = () => {
setShareFileModalOpen(true);
};
const closeShareModal = () => {
setObjectToShare(null);
setShareFileModalOpen(false);
};
const deleteTag = (tagKey: string, tagLabel: string) => {
setSelectedTag([tagKey, tagLabel]);
setDeleteTagModalOpen(true);
};
const downloadObject = (object: IFileInfo) => {
const identityDownload = encodeFileName(
`${bucketName}-${object.name}-${new Date().getTime()}-${Math.random()}`
);
setNewObject({
bucketName,
done: false,
instanceID: identityDownload,
percentage: 0,
prefix: object.name,
type: "download",
waitingForFile: true,
});
download(
bucketName,
internalPaths,
object.version_id,
parseInt(object.size || "0"),
(progress) => {
updateProgress(identityDownload, progress);
},
() => {
completeObject(identityDownload);
}
);
};
const tableActions: ItemActions[] = [
{
label: "Share",
type: "share",
onClick: (item: any) => {
setObjectToShare(item);
shareObject();
},
sendOnlyId: false,
disableButtonFunction: (item: string) => {
const element = versions.find((elm) => elm.version_id === item);
if (element && element.is_delete_marker) {
return true;
}
return false;
},
},
{
label: "Download",
type: "download",
onClick: (item: IFileInfo) => {
downloadObject(item);
},
disableButtonFunction: (item: string) => {
const element = versions.find((elm) => elm.version_id === item);
if (element && element.is_delete_marker) {
return true;
}
return false;
},
},
{
label: "Restore",
type: <RecoverIcon />,
onClick: (item: IFileInfo) => {
setRestoreVersion(item.version_id || "");
setRestoreVersionOpen(true);
},
disableButtonFunction: (item: string) => {
const element = versions.find((elm) => elm.version_id === item);
return (element && element.is_delete_marker) || false;
},
},
];
const filteredRecords = versions.filter((version) => {
if (version.version_id) {
return version.version_id.includes(filterVersion);
}
return false;
});
const displayParsedDate = (date: string) => {
return <reactMoment.default>{date}</reactMoment.default>;
};
const closeDeleteModal = (redirectBack: boolean) => {
setDeleteOpen(false);
if (redirectBack) {
const newPath = allPathData.join("/");
history.push(
`/buckets/${bucketName}/browse${
newPath === "" ? "" : `/${encodeFileName(newPath)}`
}`
);
}
};
const closeAddTagModal = (reloadObjectData: boolean) => {
setTagModalOpen(false);
if (reloadObjectData) {
setLoadObjectData(true);
}
};
const closeLegalholdModal = (reload: boolean) => {
setLegalholdOpen(false);
if (reload) {
setLoadObjectData(true);
}
};
const closeDeleteTagModal = (reloadObjectData: boolean) => {
setDeleteTagModalOpen(false);
if (reloadObjectData) {
setLoadObjectData(true);
}
};
const closeRestoreModal = (reloadObjectData: boolean) => {
setRestoreVersionOpen(false);
setRestoreVersion("");
if (reloadObjectData) {
setLoadObjectData(true);
}
};
return (
<Fragment>
{shareFileModalOpen && actualInfo && (
<ShareFile
open={shareFileModalOpen}
closeModalAndRefresh={closeShareModal}
bucketName={bucketName}
dataObject={objectToShare || actualInfo}
/>
)}
{retentionModalOpen && actualInfo && (
<SetRetention
open={retentionModalOpen}
closeModalAndRefresh={closeRetentionModal}
objectName={currentItem}
objectInfo={actualInfo}
bucketName={bucketName}
/>
)}
{deleteOpen && (
<DeleteObject
deleteOpen={deleteOpen}
selectedBucket={bucketName}
selectedObject={internalPaths}
closeDeleteModalAndRefresh={closeDeleteModal}
versioning={distributedSetup}
/>
)}
{tagModalOpen && actualInfo && (
<AddTagModal
modalOpen={tagModalOpen}
currentTags={actualInfo.tags}
selectedObject={internalPaths}
versionId={actualInfo.version_id}
bucketName={bucketName}
onCloseAndUpdate={closeAddTagModal}
/>
)}
{deleteTagModalOpen && actualInfo && (
<DeleteTagModal
deleteOpen={deleteTagModalOpen}
currentTags={actualInfo.tags}
selectedObject={actualInfo.name}
versionId={actualInfo.version_id}
bucketName={bucketName}
onCloseAndUpdate={closeDeleteTagModal}
selectedTag={selectedTag}
/>
)}
{legalholdOpen && actualInfo && (
<SetLegalHoldModal
open={legalholdOpen}
closeModalAndRefresh={closeLegalholdModal}
objectName={actualInfo.name}
bucketName={bucketName}
actualInfo={actualInfo}
/>
)}
{restoreVersionOpen && actualInfo && (
<RestoreFileVersion
restoreOpen={restoreVersionOpen}
bucketName={bucketName}
versionID={restoreVersion}
objectPath={actualInfo.name}
onCloseAndUpdate={closeRestoreModal}
/>
)}
<PageLayout className={classes.pageContainer}>
{!actualInfo && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
{actualInfo && (
<Fragment>
<Grid item xs={12}>
<ScreenTitle
icon={
<Fragment>
<ObjectBrowserIcon width={40} />
</Fragment>
}
title={
objectNameArray.length > 0
? objectNameArray[objectNameArray.length - 1]
: actualInfo.name
}
actions={
<Fragment>
<SecureComponent
scopes={[IAM_SCOPES.S3_DELETE_OBJECT]}
resource={bucketName}
matchAll
errorProps={{ disabled: true }}
>
<RBIconButton
tooltip={"Delete Object"}
onClick={() => {
setDeleteOpen(true);
}}
text={""}
icon={<DeleteIcon />}
color="secondary"
disabled={actualInfo.is_delete_marker}
variant={"outlined"}
/>
</SecureComponent>
<RBIconButton
tooltip={"Share"}
onClick={() => {
shareObject();
}}
text={""}
icon={<ShareIcon />}
color="primary"
disabled={actualInfo.is_delete_marker}
variant={"outlined"}
/>
{downloadingFiles.includes(
`${bucketName}/${actualInfo.name}`
) ? (
<div className="progressDetails">
<CircularProgress
color="primary"
size={17}
variant="indeterminate"
/>
</div>
) : (
<RBIconButton
tooltip={"Download"}
text={""}
icon={<DownloadIcon />}
color="primary"
onClick={() => {
downloadObject(actualInfo);
}}
disabled={actualInfo.is_delete_marker}
variant={"outlined"}
/>
)}
</Fragment>
}
/>
</Grid>
<VerticalTabs
classes={{
tabsContainer: classes.tabsContainer,
}}
>
{{
tabConfig: {
label: "Details",
},
content: (
<Fragment>
<div className={classes.actionsTray}>
<h1 className={classes.sectionTitle}>Details</h1>
</div>
<br />
<Box sx={{ ...twoColCssGridLayoutConfig }}>
<Box sx={{ ...twoColCssGridLayoutConfig }}>
<SecureComponent
scopes={[IAM_SCOPES.S3_GET_OBJECT_LEGAL_HOLD]}
resource={bucketName}
>
<LabelValuePair
label={""}
value={
actualInfo.version_id &&
actualInfo.version_id !== "null" ? (
<EditablePropertyItem
iamScopes={[
IAM_SCOPES.S3_PUT_OBJECT_LEGAL_HOLD,
]}
secureCmpProps={{
matchAll: false,
errorProps: {
disabled: true,
onClick: null,
},
}}
resourceName={bucketName}
property={"Legal Hold:"}
value={
actualInfo.legal_hold_status
? actualInfo.legal_hold_status.toLowerCase()
: "Off"
}
onEdit={() => {
setLegalholdOpen(true);
}}
isLoading={false}
/>
) : (
<LabelValuePair
label={"Legal Hold:"}
value={
<LabelWithIcon
icon={<DisabledIcon />}
label={
<label className={classes.textMuted}>
Disabled
</label>
}
/>
}
/>
)
}
/>
</SecureComponent>
<SecureComponent
scopes={[IAM_SCOPES.S3_GET_OBJECT_RETENTION]}
resource={bucketName}
>
<LabelValuePair
label={""}
value={
actualInfo.version_id &&
actualInfo.version_id !== "null" ? (
<EditablePropertyItem
iamScopes={[
IAM_SCOPES.S3_PUT_OBJECT_RETENTION,
]}
secureCmpProps={{
matchAll: false,
}}
resourceName={bucketName}
property={"Retention:"}
value={
actualInfo.retention_mode
? actualInfo.retention_mode.toLowerCase()
: "None"
}
onEdit={openRetentionModal}
isLoading={false}
/>
) : (
<LabelValuePair
label={"Retention:"}
value={
<LabelWithIcon
icon={<DisabledIcon />}
label={
<label className={classes.textMuted}>
Disabled
</label>
}
/>
}
/>
)
}
/>
</SecureComponent>
</Box>
</Box>
<Box className={classes.spacerTop}>
<LabelValuePair
label={"Tags:"}
value={
<ObjectTags
objectInfo={actualInfo}
tagKeys={tagKeys}
bucketName={bucketName}
onDeleteTag={deleteTag}
onAddTagClick={() => {
setTagModalOpen(true);
}}
/>
}
/>
</Box>
{actualInfo ? (
<ObjectMetaData
bucketName={bucketName}
internalPaths={internalPaths}
actualInfo={actualInfo}
/>
) : null}
</Fragment>
),
}}
{{
tabConfig: {
label: "Versions",
disabled: !(
actualInfo.version_id && actualInfo.version_id !== "null"
),
},
content: (
<Fragment>
<div className={classes.actionsTray}>
<h1 className={classes.sectionTitle}>Versions</h1>
</div>
<br />
<Grid item xs={12} className={classes.actionsTray}>
{actualInfo.version_id &&
actualInfo.version_id !== "null" && (
<SearchBox
placeholder={`Search ${currentItem}`}
onChange={setFilterVersion}
value={filterVersion}
/>
)}
</Grid>
<Grid item xs={12} className={classes.tableBlock}>
{actualInfo.version_id &&
actualInfo.version_id !== "null" && (
<TableWrapper
itemActions={tableActions}
columns={[
{
label: "",
width: 40,
renderFullObject: true,
renderFunction: (r) => {
const versOrd =
versions.length - versions.indexOf(r);
return `v${versOrd}`;
},
elementKey: "version_id",
},
{ label: "Version ID", elementKey: "version_id" },
{
label: "Last Modified",
elementKey: "last_modified",
renderFunction: displayParsedDate,
},
{
label: "Deleted",
width: 60,
contentTextAlign: "center",
renderFullObject: true,
elementKey: "is_delete_marker",
renderFunction: (r) => {
const versOrd = r.is_delete_marker
? "Yes"
: "No";
return `${versOrd}`;
},
},
]}
isLoading={false}
entityName="Versions"
idField="version_id"
records={filteredRecords}
textSelectable
/>
)}
</Grid>
</Fragment>
),
}}
{{
tabConfig: {
label: "Preview",
disabled: extensionPreview(currentItem) === "none",
},
content: (
<Fragment>
{actualInfo && (
<PreviewFileContent
bucketName={bucketName}
object={{
name: actualInfo.name,
version_id: actualInfo.version_id || "null",
size: parseInt(actualInfo.size || "0"),
content_type: "",
last_modified: new Date(actualInfo.last_modified),
}}
isFullscreen
/>
)}
</Fragment>
),
}}
</VerticalTabs>
</Fragment>
)}
</PageLayout>
</Fragment>
);
};
const mapStateToProps = ({ objectBrowser, system }: AppState) => ({
downloadingFiles: get(objectBrowser, "downloadingFiles", []),
rewindEnabled: get(objectBrowser, "rewind.rewindEnabled", false),
rewindDate: get(objectBrowser, "rewind.dateToRewind", null),
bucketToRewind: get(objectBrowser, "rewind.bucketToRewind", ""),
distributedSetup: get(system, "distributedSetup", false),
});
const mapDispatchToProps = {
setErrorSnackMessage,
setSnackBarMessage,
setNewObject,
updateProgress,
completeObject,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
export default withRouter(connector(withStyles(styles)(ObjectDetails)));

View File

@@ -1,93 +0,0 @@
import React from "react";
import { IAM_SCOPES } from "../../../../../../common/SecureComponent/permissions";
import { Box } from "@mui/material";
import get from "lodash/get";
import { SecureComponent } from "../../../../../../common/SecureComponent";
import Chip from "@mui/material/Chip";
import CloseIcon from "@mui/icons-material/Close";
import AddIcon from "@mui/icons-material/Add";
const ObjectTags = ({
tagKeys,
bucketName,
onDeleteTag,
onAddTagClick,
objectInfo,
}: {
tagKeys: any;
bucketName: string;
onDeleteTag: (key: string, v: string) => void;
onAddTagClick: () => void;
objectInfo: any;
}) => {
return (
<React.Fragment>
<SecureComponent
scopes={[IAM_SCOPES.S3_GET_OBJECT_TAGGING]}
resource={bucketName}
>
<Box
sx={{
display: "flex",
flexFlow: "column",
}}
>
<Box>
{tagKeys &&
tagKeys.map((tagKey: string, index: number) => {
const tag = get(objectInfo, `tags.${tagKey}`, "");
if (tag !== "") {
return (
<SecureComponent
key={`chip-${index}`}
scopes={[IAM_SCOPES.S3_DELETE_OBJECT_TAGGING]}
resource={bucketName}
matchAll
errorProps={{
deleteIcon: null,
onDelete: null,
}}
>
<Chip
style={{
textTransform: "none",
marginRight: "5px",
}}
size="small"
label={`${tagKey} : ${tag}`}
color="primary"
deleteIcon={<CloseIcon />}
onDelete={() => {
onDeleteTag(tagKey, tag);
}}
/>
</SecureComponent>
);
}
return null;
})}
</Box>
<SecureComponent
scopes={[IAM_SCOPES.S3_PUT_OBJECT_TAGGING]}
resource={bucketName}
errorProps={{ disabled: true, onClick: null }}
>
<Chip
style={{ maxWidth: 80, marginTop: "10px" }}
icon={<AddIcon />}
clickable
size="small"
label="Add tag"
color="primary"
variant="outlined"
onClick={onAddTagClick}
/>
</SecureComponent>
</Box>
</SecureComponent>
</React.Fragment>
);
};
export default ObjectTags;

View File

@@ -0,0 +1,322 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useState, Fragment } from "react";
import get from "lodash/get";
import { connect } from "react-redux";
import { Box, Button, Grid } from "@mui/material";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { setModalErrorSnackMessage } from "../../../../../../actions";
import { AppState } from "../../../../../../store";
import { ErrorResponseHandler } from "../../../../../../common/types";
import InputBoxWrapper from "../../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import ModalWrapper from "../../../../Common/ModalWrapper/ModalWrapper";
import api from "../../../../../../common/api";
import { encodeFileName } from "../../../../../../common/utils";
import {
formFieldStyles,
modalStyleUtils,
spacingUtils,
} from "../../../../Common/FormComponents/common/styleLibrary";
import { TagsIcon } from "../../../../../../icons";
import { IFileInfo } from "./types";
import { IAM_SCOPES } from "../../../../../../common/SecureComponent/permissions";
import { SecureComponent } from "../../../../../../common/SecureComponent";
import Chip from "@mui/material/Chip";
import CloseIcon from "@mui/icons-material/Close";
interface ITagModal {
modalOpen: boolean;
bucketName: string;
actualInfo: IFileInfo;
onCloseAndUpdate: (refresh: boolean) => void;
distributedSetup: boolean;
setModalErrorSnackMessage: typeof setModalErrorSnackMessage;
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
pathLabel: {
marginTop: 0,
marginBottom: 32,
},
newTileHeader: {
fontSize: 18,
fontWeight: "bold",
color: "#000",
margin: "20px 0",
paddingBottom: 15,
borderBottom: "#E2E2E2 2px solid",
},
...formFieldStyles,
...modalStyleUtils,
...spacingUtils,
});
const AddTagModal = ({
modalOpen,
onCloseAndUpdate,
bucketName,
distributedSetup,
actualInfo,
setModalErrorSnackMessage,
classes,
}: ITagModal) => {
const [newKey, setNewKey] = useState<string>("");
const [newLabel, setNewLabel] = useState<string>("");
const [isSending, setIsSending] = useState<boolean>(false);
const [deleteEnabled, setDeleteEnabled] = useState<boolean>(false);
const [deleteKey, setDeleteKey] = useState<string>("");
const [deleteLabel, setDeleteLabel] = useState<string>("");
const selectedObject = encodeFileName(actualInfo.name);
const currentTags = actualInfo.tags;
const currTagKeys = Object.keys(currentTags || {});
const allPathData = actualInfo.name.split("/");
const currentItem = allPathData.pop() || "";
const resetForm = () => {
setNewLabel("");
setNewKey("");
};
const addTagProcess = () => {
setIsSending(true);
const newTag: any = {};
newTag[newKey] = newLabel;
const newTagList = { ...currentTags, ...newTag };
const verID = distributedSetup ? actualInfo.version_id : "null";
api
.invoke(
"PUT",
`/api/v1/buckets/${bucketName}/objects/tags?prefix=${selectedObject}&version_id=${verID}`,
{ tags: newTagList }
)
.then((res: any) => {
onCloseAndUpdate(true);
setIsSending(false);
})
.catch((error: ErrorResponseHandler) => {
setModalErrorSnackMessage(error);
setIsSending(false);
});
};
const deleteTagProcess = () => {
const cleanObject: any = { ...currentTags };
delete cleanObject[deleteKey];
const verID = distributedSetup ? actualInfo.version_id : "null";
api
.invoke(
"PUT",
`/api/v1/buckets/${bucketName}/objects/tags?prefix=${selectedObject}&version_id=${verID}`,
{ tags: cleanObject }
)
.then((res: any) => {
onCloseAndUpdate(true);
setIsSending(false);
})
.catch((error: ErrorResponseHandler) => {
setModalErrorSnackMessage(error);
setIsSending(false);
});
};
const onDeleteTag = (tagKey: string, tag: string) => {
setDeleteKey(tagKey);
setDeleteLabel(tag);
setDeleteEnabled(true);
};
const cancelDelete = () => {
setDeleteKey("");
setDeleteLabel("");
setDeleteEnabled(false);
};
return (
<Fragment>
<ModalWrapper
modalOpen={modalOpen}
title={deleteEnabled ? `Delete Tag` : `Edit Tags for ${currentItem}`}
onClose={() => {
onCloseAndUpdate(true);
}}
titleIcon={<TagsIcon />}
>
{deleteEnabled ? (
<Fragment>
<Grid container>
Are you sure you want to delete the tag{" "}
<b className={classes.wrapText}>
{deleteKey} : {deleteLabel}
</b>{" "}
from {currentItem}?
<Grid item xs={12} className={classes.modalButtonBar}>
<Button
type="button"
variant="outlined"
color="primary"
onClick={cancelDelete}
>
No
</Button>
<Button
type="submit"
variant="outlined"
color="secondary"
onClick={deleteTagProcess}
>
Yes
</Button>
</Grid>
</Grid>
</Fragment>
) : (
<Grid container>
<SecureComponent
scopes={[IAM_SCOPES.S3_GET_OBJECT_TAGGING]}
resource={bucketName}
>
<Box
sx={{
display: "flex",
flexFlow: "column",
}}
>
<strong>Current Tags:</strong>
{currTagKeys.length === 0 ? "No Tags for this object" : ""}
<Box>
{currTagKeys.map((tagKey: string, index: number) => {
const tag = get(currentTags, `${tagKey}`, "");
if (tag !== "") {
return (
<SecureComponent
key={`chip-${index}`}
scopes={[IAM_SCOPES.S3_DELETE_OBJECT_TAGGING]}
resource={bucketName}
matchAll
errorProps={{
deleteIcon: null,
onDelete: null,
}}
>
<Chip
style={{
textTransform: "none",
marginRight: "5px",
}}
size="small"
label={`${tagKey} : ${tag}`}
color="primary"
deleteIcon={<CloseIcon />}
onDelete={() => {
onDeleteTag(tagKey, tag);
}}
/>
</SecureComponent>
);
}
return null;
})}
</Box>
</Box>
</SecureComponent>
<SecureComponent
scopes={[IAM_SCOPES.S3_PUT_OBJECT_TAGGING]}
resource={bucketName}
errorProps={{ disabled: true, onClick: null }}
>
<Grid container>
<Grid item xs={12} className={classes.newTileHeader}>
Add New Tag
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
value={newKey}
label={"Tag Key"}
id={"newTagKey"}
name={"newTagKey"}
placeholder={"Enter Tag Key"}
onChange={(e) => {
setNewKey(e.target.value);
}}
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
value={newLabel}
label={"Tag Label"}
id={"newTagLabel"}
name={"newTagLabel"}
placeholder={"Enter Tag Label"}
onChange={(e) => {
setNewLabel(e.target.value);
}}
/>
</Grid>
<Grid item xs={12} className={classes.modalButtonBar}>
<Button
type="button"
variant="outlined"
color="primary"
onClick={resetForm}
>
Clear
</Button>
<Button
type="submit"
variant="contained"
color="primary"
disabled={
newLabel.trim() === "" ||
newKey.trim() === "" ||
isSending
}
onClick={addTagProcess}
>
Save new Tag
</Button>
</Grid>
</Grid>
</SecureComponent>
</Grid>
)}
</ModalWrapper>
</Fragment>
);
};
const mapStateToProps = ({ system }: AppState) => ({
distributedSetup: get(system, "distributedSetup", false),
});
const mapDispatchToProps = {
setModalErrorSnackMessage,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
export default withStyles(styles)(connector(AddTagModal));

View File

@@ -23,6 +23,7 @@ export interface IFileInfo {
retention_until_date?: string;
size?: string;
tags?: object;
etag?: string;
version_id: string | null;
is_delete_marker?: boolean;
user_metadata?: object;

View File

@@ -1332,7 +1332,11 @@ export const detailsPanel: any = {
fontSize: 14,
fontWeight: "bold",
color: "#000",
padding: "12px 22px 8px 22px",
padding: "12px 30px 8px 22px",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
alignItems: "center",
},
objectActions: {
backgroundColor: "#F8F8F8",

File diff suppressed because it is too large Load Diff

View File

@@ -4353,6 +4353,9 @@ func init() {
"content_type": {
"type": "string"
},
"etag": {
"type": "string"
},
"expiration": {
"type": "string"
},
@@ -10937,6 +10940,9 @@ func init() {
"content_type": {
"type": "string"
},
"etag": {
"type": "string"
},
"expiration": {
"type": "string"
},

View File

@@ -244,6 +244,7 @@ func listBucketObjects(ctx context.Context, client MinioClient, bucketName strin
IsDeleteMarker: lsObj.IsDeleteMarker,
UserTags: lsObj.UserTags,
UserMetadata: lsObj.UserMetadata,
Etag: lsObj.ETag,
}
// only if single object with or without versions; get legalhold, retention and tags
if !lsObj.IsDeleteMarker && prefix != "" && !strings.HasSuffix(prefix, "/") {

View File

@@ -2793,6 +2793,8 @@ definitions:
type: string
retention_until_date:
type: string
etag:
type: string
tags:
type: object
additionalProperties: