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:
@@ -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"`
|
||||
|
||||
|
||||
35
portal-ui/src/icons/LegalHoldIcon.tsx
Normal file
35
portal-ui/src/icons/LegalHoldIcon.tsx
Normal 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;
|
||||
39
portal-ui/src/icons/MetadataIcon.tsx
Normal file
39
portal-ui/src/icons/MetadataIcon.tsx
Normal 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;
|
||||
35
portal-ui/src/icons/ObjectInfoIcon.tsx
Normal file
35
portal-ui/src/icons/ObjectInfoIcon.tsx
Normal 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;
|
||||
39
portal-ui/src/icons/RetentionIcon.tsx
Normal file
39
portal-ui/src/icons/RetentionIcon.tsx
Normal 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;
|
||||
35
portal-ui/src/icons/TagsIcon.tsx
Normal file
35
portal-ui/src/icons/TagsIcon.tsx
Normal 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;
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
@@ -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:"}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
export interface BucketObject {
|
||||
name: string;
|
||||
size: number;
|
||||
etag?: string;
|
||||
last_modified: Date;
|
||||
content_type: string;
|
||||
version_id: string;
|
||||
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
@@ -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)));
|
||||
@@ -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;
|
||||
@@ -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));
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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, "/") {
|
||||
|
||||
@@ -2793,6 +2793,8 @@ definitions:
|
||||
type: string
|
||||
retention_until_date:
|
||||
type: string
|
||||
etag:
|
||||
type: string
|
||||
tags:
|
||||
type: object
|
||||
additionalProperties:
|
||||
|
||||
Reference in New Issue
Block a user