From 69a3ee6c1a095d4b8356922ec5e16edc1351c65e Mon Sep 17 00:00:00 2001 From: Alex <33497058+bexsoft@users.noreply.github.com> Date: Mon, 28 Feb 2022 23:30:13 -0700 Subject: [PATCH] Updated design of object details panel (#1637) - Removed old references to object details - Created new Tags edit module Signed-off-by: Benjamin Perez Co-authored-by: Benjamin Perez --- models/bucket_object.go | 3 + portal-ui/src/icons/LegalHoldIcon.tsx | 35 + portal-ui/src/icons/MetadataIcon.tsx | 39 + portal-ui/src/icons/ObjectInfoIcon.tsx | 35 + portal-ui/src/icons/RetentionIcon.tsx | 39 + portal-ui/src/icons/TagsIcon.tsx | 35 + portal-ui/src/icons/index.ts | 5 + ...ectionPanel.tsx => ActionsListSection.tsx} | 14 +- .../Objects/ListObjects/ListObjects.tsx | 6 +- .../Objects/ListObjects/ObjectDetailPanel.tsx | 532 +++++------ .../ListBuckets/Objects/ListObjects/types.tsx | 1 + .../Objects/ObjectDetails/AddTagModal.tsx | 181 ---- .../Objects/ObjectDetails/DeleteTagModal.tsx | 118 --- .../Objects/ObjectDetails/ObjectDetails.tsx | 843 ------------------ .../Objects/ObjectDetails/ObjectTags.tsx | 93 -- .../Objects/ObjectDetails/TagsModal.tsx | 322 +++++++ .../Objects/ObjectDetails/types.ts | 1 + .../FormComponents/common/styleLibrary.ts | 6 +- .../screens/Console/Common/IconsScreen.tsx | 558 ++++++++---- restapi/embedded_spec.go | 6 + restapi/user_objects.go | 1 + swagger-console.yml | 2 + 22 files changed, 1195 insertions(+), 1680 deletions(-) create mode 100644 portal-ui/src/icons/LegalHoldIcon.tsx create mode 100644 portal-ui/src/icons/MetadataIcon.tsx create mode 100644 portal-ui/src/icons/ObjectInfoIcon.tsx create mode 100644 portal-ui/src/icons/RetentionIcon.tsx create mode 100644 portal-ui/src/icons/TagsIcon.tsx rename portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/{MultiSelectionPanel.tsx => ActionsListSection.tsx} (85%) delete mode 100644 portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/AddTagModal.tsx delete mode 100644 portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/DeleteTagModal.tsx delete mode 100644 portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx delete mode 100644 portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectTags.tsx create mode 100644 portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/TagsModal.tsx diff --git a/models/bucket_object.go b/models/bucket_object.go index 6d1ab1133..e8f34abc2 100644 --- a/models/bucket_object.go +++ b/models/bucket_object.go @@ -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"` diff --git a/portal-ui/src/icons/LegalHoldIcon.tsx b/portal-ui/src/icons/LegalHoldIcon.tsx new file mode 100644 index 000000000..659135760 --- /dev/null +++ b/portal-ui/src/icons/LegalHoldIcon.tsx @@ -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 . + +import * as React from "react"; +import { SVGProps } from "react"; + +const LegalHoldIcon = (props: SVGProps) => ( + + + +); + +export default LegalHoldIcon; diff --git a/portal-ui/src/icons/MetadataIcon.tsx b/portal-ui/src/icons/MetadataIcon.tsx new file mode 100644 index 000000000..4d2285d77 --- /dev/null +++ b/portal-ui/src/icons/MetadataIcon.tsx @@ -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 . + +import * as React from "react"; +import { SVGProps } from "react"; + +const MetadataIcon = (props: SVGProps) => ( + + + + +); + +export default MetadataIcon; diff --git a/portal-ui/src/icons/ObjectInfoIcon.tsx b/portal-ui/src/icons/ObjectInfoIcon.tsx new file mode 100644 index 000000000..c4ccad51f --- /dev/null +++ b/portal-ui/src/icons/ObjectInfoIcon.tsx @@ -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 . + +import * as React from "react"; +import { SVGProps } from "react"; + +const ObjectInfoIcon = (props: SVGProps) => ( + + + +); + +export default ObjectInfoIcon; diff --git a/portal-ui/src/icons/RetentionIcon.tsx b/portal-ui/src/icons/RetentionIcon.tsx new file mode 100644 index 000000000..42815d364 --- /dev/null +++ b/portal-ui/src/icons/RetentionIcon.tsx @@ -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 . + +import * as React from "react"; +import { SVGProps } from "react"; + +const RetentionIcon = (props: SVGProps) => ( + + + + +); + +export default RetentionIcon; diff --git a/portal-ui/src/icons/TagsIcon.tsx b/portal-ui/src/icons/TagsIcon.tsx new file mode 100644 index 000000000..73afb9aab --- /dev/null +++ b/portal-ui/src/icons/TagsIcon.tsx @@ -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 . + +import * as React from "react"; +import { SVGProps } from "react"; + +const TagsIcon = (props: SVGProps) => ( + + + +); + +export default TagsIcon; diff --git a/portal-ui/src/icons/index.ts b/portal-ui/src/icons/index.ts index 675a8572a..ba1199806 100644 --- a/portal-ui/src/icons/index.ts +++ b/portal-ui/src/icons/index.ts @@ -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"; diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/MultiSelectionPanel.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ActionsListSection.tsx similarity index 85% rename from portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/MultiSelectionPanel.tsx rename to portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ActionsListSection.tsx index 4ddde5fc1..3d379119b 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/MultiSelectionPanel.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ActionsListSection.tsx @@ -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 (
{title}
  • Actions:
  • - {items.map((actionItem) => { + {items.map((actionItem, index) => { return ( -
  • +
  • { + setSelectedInternalPaths(null); + if (selectedObjects.length === payload.length) { setSelectedObjects([]); return; @@ -1312,7 +1314,7 @@ const ListObjects = ({ }} > {selectedObjects.length > 0 && ( - diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ObjectDetailPanel.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ObjectDetailPanel.tsx index 2ab63da35..53f171193 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ObjectDetailPanel.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ObjectDetailPanel.tsx @@ -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(false); const [retentionModalOpen, setRetentionModalOpen] = useState(false); const [tagModalOpen, setTagModalOpen] = useState(false); - const [deleteTagModalOpen, setDeleteTagModalOpen] = useState(false); - const [selectedTag, setSelectedTag] = useState(["", ""]); const [legalholdOpen, setLegalholdOpen] = useState(false); const [actualInfo, setActualInfo] = useState(null); const [allInfoElements, setAllInfoElements] = useState([]); @@ -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: , + tooltip: "Download this Object", + }, + { + action: () => { + shareObject(); + }, + label: "Share", + disabled: !!actualInfo.is_delete_marker, + icon: , + tooltip: "Share this File", + }, + { + action: () => { + setPreviewOpen(true); + }, + label: "Preview", + disabled: + !!actualInfo.is_delete_marker || + extensionPreview(currentItem) === "none", + icon: , + 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: , + 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: , + tooltip: "Change Retention rules for this File", + }, + { + action: () => { + setTagModalOpen(true); + }, + label: "Tags", + disabled: + !!actualInfo.is_delete_marker || + extensionPreview(currentItem) === "none" || + selectedVersion !== "", + icon: , + tooltip: "Change Tags for this File", + }, + { + action: () => { + setVersionsModeEnabled(!versionsMode, objectName); + }, + label: versionsMode ? "Hide Object Versions" : "Display Object Versions", + icon: , + disabled: !(actualInfo.version_id && actualInfo.version_id !== "null"), + tooltip: "Display Versions for this file", + }, + ]; + + /* + * + * + * + {selectedVersion === "" ? ( + { + setTagModalOpen(true); + }} + /> + } + /> + ) : ( + + Tags: +
    + +
    + )} +
    + * + * */ + 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 ( {shareFileModalOpen && actualInfo && ( @@ -389,27 +520,6 @@ const ObjectDetailPanel = ({ versioning={distributedSetup} /> )} - {tagModalOpen && actualInfo && ( - - )} - {deleteTagModalOpen && actualInfo && ( - - )} {legalholdOpen && actualInfo && ( )} + {tagModalOpen && actualInfo && ( + + )} {!actualInfo && ( @@ -449,76 +567,55 @@ const ObjectDetailPanel = ({ )} -
    {objectName}
    + + {displayFileIconName(objectName, true)} + {objectName} + + } + items={multiActionButtons} + /> -
      -
    • Actions:
    • -
    • - } - onClick={() => { - downloadObject(actualInfo); - }} - disabled={actualInfo.is_delete_marker} - /> -
    • -
    • - } - onClick={() => { - shareObject(); - }} - disabled={actualInfo.is_delete_marker} - /> -
    • -
    • - } - onClick={() => { - setPreviewOpen(true); - }} - disabled={actualInfo.is_delete_marker} - /> -
    • - -
    • - } + + {selectedVersion === "" && ( + +
    • -
      -
    • - } - onClick={() => { - setVersionsModeEnabled(!versionsMode, objectName); - }} - disabled={ - !(actualInfo.version_id && actualInfo.version_id !== "null") - } - /> -
    • -
    - -
    -

    Details

    -
    + sx={{ + width: "calc(100% - 44px)", + margin: "8px 0", + "& svg.min-icon": { + width: 14, + height: 14, + }, + }} + > + Delete + + + )} + + + Object Info + + + + Name: +
    + {objectName} +
    {selectedVersion !== "" && ( Version ID: @@ -527,91 +624,56 @@ const ObjectDetailPanel = ({ )} - {selectedVersion === "" ? ( - { - setTagModalOpen(true); - }} - /> - } - /> - ) : ( - - Tags: + Size: +
    + {niceBytes(actualInfo.size || "0")} +
    + {actualInfo.version_id && + actualInfo.version_id !== "null" && + selectedVersion === "" && ( + + Versions:
    - {tagKeys.length === 0 - ? "N/A" - : tagKeys.map((tagKey, index) => { - return ( - - {tagKey}:{get(actualInfo, `tags.${tagKey}`, "")} - {index < tagKeys.length - 1 ? ", " : ""} - - ); - })} -
    + {versions.length} version{versions.length !== 1 ? "s" : ""},{" "} + {niceBytesInt(totalVersionsSize)} + )} + {selectedVersion === "" && ( + + Last Modified: +
    + {calculateLastModifyTime(actualInfo.last_modified)} +
    + )} + + ETAG: +
    + {actualInfo.etag || "N/A"} +
    + + Tags: +
    + {tagKeys.length === 0 + ? "N/A" + : tagKeys.map((tagKey, index) => { + return ( + + {tagKey}:{get(actualInfo, `tags.${tagKey}`, "")} + {index < tagKeys.length - 1 ? ", " : ""} + + ); + })}
    - {selectedVersion === "" ? ( - { - setLegalholdOpen(true); - }} - isLoading={false} - /> - ) : ( - } - label={ - - } - /> - } - /> - ) - } - /> - ) : ( - - Legal Hold: -
    - {actualInfo.legal_hold_status ? "On" : "Off"} -
    - )} + + Legal Hold: +
    + {actualInfo.legal_hold_status ? "On" : "Off"} +
    @@ -619,56 +681,31 @@ const ObjectDetailPanel = ({ scopes={[IAM_SCOPES.S3_GET_OBJECT_RETENTION]} resource={bucketName} > - {selectedVersion === "" ? ( - - ) : ( - } - label={ - - } - /> - } - /> - ) - } - /> - ) : ( - - Object Retention: -
    - {actualInfo.retention_mode - ? actualInfo.retention_mode.toLowerCase() - : "None"} -
    - )} + + Retention Policy: +
    + + {actualInfo.version_id && actualInfo.version_id !== "null" ? ( + + {actualInfo.retention_mode + ? actualInfo.retention_mode.toLowerCase() + : "None"} + + ) : ( + + {actualInfo.retention_mode + ? actualInfo.retention_mode.toLowerCase() + : "None"} + + )} + +
    -
    -
    -

    Object Metadata

    -
    + + Metadata + + {actualInfo ? ( ) : null} -
    - - {actualInfo.version_id && - actualInfo.version_id !== "null" && - selectedVersion === "" && ( - -
    -

    Versions

    -
    - - - Total available versions -
    - {versions.length} -
    - - Versions Stored size: -
    - {niceBytesInt(totalVersionsSize)} -
    -
    -
    - )} ); }; diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/types.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/types.tsx index 340b302e0..b57d618a7 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/types.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/types.tsx @@ -17,6 +17,7 @@ export interface BucketObject { name: string; size: number; + etag?: string; last_modified: Date; content_type: string; version_id: string; diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/AddTagModal.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/AddTagModal.tsx deleted file mode 100644 index 14069b77f..000000000 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/AddTagModal.tsx +++ /dev/null @@ -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 . - -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(""); - const [newLabel, setNewLabel] = useState(""); - const [isSending, setIsSending] = useState(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 ( - - { - onCloseAndUpdate(true); - }} - titleIcon={} - > - -
    - Selected Object: {decodeFileName(selectedObject)} -
    - - { - setNewKey(e.target.value); - }} - /> - - - { - setNewLabel(e.target.value); - }} - /> - - - - - -
    -
    -
    - ); -}; - -const mapStateToProps = ({ system }: AppState) => ({ - distributedSetup: get(system, "distributedSetup", false), -}); - -const mapDispatchToProps = { - setModalErrorSnackMessage, -}; - -const connector = connect(mapStateToProps, mapDispatchToProps); - -export default withStyles(styles)(connector(AddTagModal)); diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/DeleteTagModal.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/DeleteTagModal.tsx deleted file mode 100644 index 10e84f16e..000000000 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/DeleteTagModal.tsx +++ /dev/null @@ -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 . - -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 ( - } - isLoading={deleteLoading} - onConfirm={onConfirmDelete} - onClose={onClose} - confirmationContent={ - - Are you sure you want to delete the tag{" "} - - {tagKey} : {tagLabel} - {" "} - from {selectedObject}? - - } - /> - ); -}; - -const mapStateToProps = ({ system }: AppState) => ({ - distributedSetup: get(system, "distributedSetup", false), -}); - -const mapDispatchToProps = { - setErrorSnackMessage, -}; - -const connector = connect(mapStateToProps, mapDispatchToProps); - -export default withStyles(styles)(connector(DeleteTagModal)); diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx deleted file mode 100644 index 8db153ed8..000000000 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx +++ /dev/null @@ -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 . - -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(true); - const [shareFileModalOpen, setShareFileModalOpen] = useState(false); - const [retentionModalOpen, setRetentionModalOpen] = useState(false); - const [tagModalOpen, setTagModalOpen] = useState(false); - const [deleteTagModalOpen, setDeleteTagModalOpen] = useState(false); - const [selectedTag, setSelectedTag] = useState(["", ""]); - const [legalholdOpen, setLegalholdOpen] = useState(false); - const [actualInfo, setActualInfo] = useState(null); - const [objectToShare, setObjectToShare] = useState(null); - const [versions, setVersions] = useState([]); - const [filterVersion, setFilterVersion] = useState(""); - const [deleteOpen, setDeleteOpen] = useState(false); - const [restoreVersionOpen, setRestoreVersionOpen] = useState(false); - const [restoreVersion, setRestoreVersion] = useState(""); - - 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: , - 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 {date}; - }; - - 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 ( - - {shareFileModalOpen && actualInfo && ( - - )} - {retentionModalOpen && actualInfo && ( - - )} - {deleteOpen && ( - - )} - {tagModalOpen && actualInfo && ( - - )} - {deleteTagModalOpen && actualInfo && ( - - )} - {legalholdOpen && actualInfo && ( - - )} - {restoreVersionOpen && actualInfo && ( - - )} - - - {!actualInfo && ( - - - - )} - - {actualInfo && ( - - - - - - } - title={ - objectNameArray.length > 0 - ? objectNameArray[objectNameArray.length - 1] - : actualInfo.name - } - actions={ - - - { - setDeleteOpen(true); - }} - text={""} - icon={} - color="secondary" - disabled={actualInfo.is_delete_marker} - variant={"outlined"} - /> - - - { - shareObject(); - }} - text={""} - icon={} - color="primary" - disabled={actualInfo.is_delete_marker} - variant={"outlined"} - /> - - {downloadingFiles.includes( - `${bucketName}/${actualInfo.name}` - ) ? ( -
    - -
    - ) : ( - } - color="primary" - onClick={() => { - downloadObject(actualInfo); - }} - disabled={actualInfo.is_delete_marker} - variant={"outlined"} - /> - )} -
    - } - /> - - - {{ - tabConfig: { - label: "Details", - }, - content: ( - -
    -

    Details

    -
    -
    - - - - - { - setLegalholdOpen(true); - }} - isLoading={false} - /> - ) : ( - } - label={ - - } - /> - } - /> - ) - } - /> - - - - - ) : ( - } - label={ - - } - /> - } - /> - ) - } - /> - - - - - - { - setTagModalOpen(true); - }} - /> - } - /> - - - {actualInfo ? ( - - ) : null} -
    - ), - }} - {{ - tabConfig: { - label: "Versions", - disabled: !( - actualInfo.version_id && actualInfo.version_id !== "null" - ), - }, - content: ( - -
    -

    Versions

    -
    -
    - - {actualInfo.version_id && - actualInfo.version_id !== "null" && ( - - )} - - - {actualInfo.version_id && - actualInfo.version_id !== "null" && ( - { - 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 - /> - )} - -
    - ), - }} - {{ - tabConfig: { - label: "Preview", - disabled: extensionPreview(currentItem) === "none", - }, - content: ( - - {actualInfo && ( - - )} - - ), - }} -
    -
    - )} - - - ); -}; - -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))); diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectTags.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectTags.tsx deleted file mode 100644 index 2f91d03b3..000000000 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectTags.tsx +++ /dev/null @@ -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 ( - - - - - {tagKeys && - tagKeys.map((tagKey: string, index: number) => { - const tag = get(objectInfo, `tags.${tagKey}`, ""); - if (tag !== "") { - return ( - - } - onDelete={() => { - onDeleteTag(tagKey, tag); - }} - /> - - ); - } - return null; - })} - - - - } - clickable - size="small" - label="Add tag" - color="primary" - variant="outlined" - onClick={onAddTagClick} - /> - - - - - ); -}; - -export default ObjectTags; diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/TagsModal.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/TagsModal.tsx new file mode 100644 index 000000000..a76e90b45 --- /dev/null +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/TagsModal.tsx @@ -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 . + +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(""); + const [newLabel, setNewLabel] = useState(""); + const [isSending, setIsSending] = useState(false); + const [deleteEnabled, setDeleteEnabled] = useState(false); + const [deleteKey, setDeleteKey] = useState(""); + const [deleteLabel, setDeleteLabel] = useState(""); + + 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 ( + + { + onCloseAndUpdate(true); + }} + titleIcon={} + > + {deleteEnabled ? ( + + + Are you sure you want to delete the tag{" "} + + {deleteKey} : {deleteLabel} + {" "} + from {currentItem}? + + + + + + + ) : ( + + + + Current Tags: + {currTagKeys.length === 0 ? "No Tags for this object" : ""} + + {currTagKeys.map((tagKey: string, index: number) => { + const tag = get(currentTags, `${tagKey}`, ""); + if (tag !== "") { + return ( + + } + onDelete={() => { + onDeleteTag(tagKey, tag); + }} + /> + + ); + } + return null; + })} + + + + + + + Add New Tag + + + { + setNewKey(e.target.value); + }} + /> + + + { + setNewLabel(e.target.value); + }} + /> + + + + + + + + + )} + + + ); +}; + +const mapStateToProps = ({ system }: AppState) => ({ + distributedSetup: get(system, "distributedSetup", false), +}); + +const mapDispatchToProps = { + setModalErrorSnackMessage, +}; + +const connector = connect(mapStateToProps, mapDispatchToProps); + +export default withStyles(styles)(connector(AddTagModal)); diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/types.ts b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/types.ts index a560a7c37..2b406494c 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/types.ts +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/types.ts @@ -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; diff --git a/portal-ui/src/screens/Console/Common/FormComponents/common/styleLibrary.ts b/portal-ui/src/screens/Console/Common/FormComponents/common/styleLibrary.ts index b95c71f3f..85f7a4842 100644 --- a/portal-ui/src/screens/Console/Common/FormComponents/common/styleLibrary.ts +++ b/portal-ui/src/screens/Console/Common/FormComponents/common/styleLibrary.ts @@ -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", diff --git a/portal-ui/src/screens/Console/Common/IconsScreen.tsx b/portal-ui/src/screens/Console/Common/IconsScreen.tsx index 6b42498dd..222a975fb 100644 --- a/portal-ui/src/screens/Console/Common/IconsScreen.tsx +++ b/portal-ui/src/screens/Console/Common/IconsScreen.tsx @@ -82,16 +82,19 @@ const IconsScreen = ({ classes }: IIconsScreenSimple) => { })} > -
    + +
    ConsoleLogo
    -
    + +
    LoginMinIOLogo
    -
    + +
    OperatorLogo
    @@ -105,757 +108,938 @@ const IconsScreen = ({ classes }: IIconsScreenSimple) => { })} > -
    + +
    AccountIcon
    -
    + +
    AddAccessRuleIcon
    -
    + +
    AddFolderIcon
    -
    + +
    AddIcon
    -
    + +
    AddMembersToGroupIcon
    -
    + +
    AddNewTagIcon
    -
    + +
    AllBucketsIcon
    -
    + +
    ArrowIcon
    -
    + +
    ArrowRightIcon
    -
    + +
    AzureTierIcon
    -
    + +
    AzureTierIconXs
    -
    + +
    BackSettingsIcon
    -
    + +
    BucketEncryptionIcon
    -
    + +
    BucketQuotaIcon
    -
    + +
    BucketReplicationIcon
    -
    + +
    BucketsIcon
    -
    + +
    CalendarIcon
    -
    + +
    CallHomeFeatureIcon
    -
    + +
    ChangeAccessPolicyIcon
    -
    + +
    ChangePasswordIcon
    -
    + +
    CircleIcon
    -
    + +
    ClosePanelIcon
    -
    + +
    ClustersIcon
    -
    + +
    CollapseIcon
    -
    + +
    ComputerLineIcon
    -
    + +
    ConfigurationsListIcon
    -
    + +
    ConfirmDeleteIcon
    -
    + +
    ConfirmModalIcon
    -
    + +
    ConsoleIcon
    -
    + +
    CopyIcon
    -
    + +
    CreateGroupIcon
    -
    + +
    CreateIcon
    -
    + +
    CreateNewPathIcon
    -
    + +
    CreateUserIcon
    -
    + +
    DashboardIcon
    -
    + +
    DeleteIcon
    -
    + +
    DiagnosticsFeatureIcon
    -
    + +
    DiagnosticsIcon
    -
    + +
    DisabledIcon
    -
    + +
    DocumentationIcon
    -
    + +
    DownloadIcon
    -
    + +
    DownloadStatIcon
    -
    + +
    DriveFormatErrorsIcon
    -
    + +
    DrivesIcon
    -
    + +
    EditIcon
    -
    + +
    EditYamlIcon
    -
    + +
    EditorThemeSwitchIcon
    -
    + +
    EgressIcon
    -
    + +
    EnabledIcon
    -
    + +
    EventSubscriptionIcon
    -
    + +
    FileBookIcon
    -
    + +
    FileCloudIcon
    -
    + +
    FileCodeIcon
    -
    + +
    FileConfigIcon
    -
    + +
    FileDbIcon
    -
    + +
    FileFontIcon
    -
    + +
    FileImageIcon
    -
    + +
    FileLinkIcon
    -
    + +
    FileLockIcon
    -
    + +
    FileMissingIcon
    -
    + +
    FileMusicIcon
    -
    + +
    FilePdfIcon
    -
    + +
    FilePptIcon
    -
    + +
    FileTxtIcon
    -
    + +
    FileVideoIcon
    -
    + +
    FileWorldIcon
    -
    + +
    FileXlsIcon
    -
    + +
    FileZipIcon
    -
    + +
    FolderIcon
    -
    + +
    FormatDrivesIcon
    -
    + +
    GoogleTierIcon
    -
    + +
    GoogleTierIconXs
    -
    + +
    GroupsIcon
    -
    + +
    HardBucketQuotaIcon
    -
    + +
    HealIcon
    -
    + +
    HelpIcon
    -
    + +
    HelpIconFilled
    -
    + +
    HistoryIcon
    -
    + +
    IAMPoliciesIcon
    -
    + +
    JSONIcon
    -
    + +
    LambdaBalloonIcon
    -
    + +
    LambdaIcon
    -
    + +
    LambdaNotificationsIcon
    -
    + +
    + LegalHoldIcon +
    + + + +
    LicenseIcon
    -
    + +
    LifecycleConfigIcon
    -
    + +
    LockIcon
    -
    + +
    LogoutIcon
    -
    + +
    LogsIcon
    -
    + +
    + MetadataIcon +
    + + + +
    MinIOTierIcon
    -
    + +
    MinIOTierIconXs
    -
    + +
    MirroringIcon
    -
    + +
    MultipleBucketsIcon
    -
    + +
    NewAccountIcon
    -
    + +
    NewPathIcon
    -
    + +
    NewPoolIcon
    -
    + +
    NextArrowIcon
    -
    + +
    ObjectBrowser1Icon
    -
    + +
    ObjectBrowserFolderIcon
    -
    + +
    ObjectBrowserIcon
    -
    + +
    + ObjectInfoIcon +
    + + + +
    ObjectManagerIcon
    -
    + +
    ObjectPreviewIcon
    -
    + +
    OfflineRegistrationBackIcon
    -
    + +
    OfflineRegistrationIcon
    -
    + +
    OnlineRegistrationBackIcon
    -
    + +
    OnlineRegistrationIcon
    -
    + +
    OpenListIcon
    -
    + +
    PasswordKeyIcon
    -
    + +
    PerformanceFeatureIcon
    -
    + +
    PermissionIcon
    -
    + +
    PreviewIcon
    -
    + +
    PrometheusErrorIcon
    -
    + +
    PrometheusIcon
    -
    + +
    RecoverIcon
    -
    + +
    RedoIcon
    -
    + +
    RefreshIcon
    -
    + +
    RemoveIcon
    -
    + +
    ReportedUsageFullIcon
    -
    + +
    ReportedUsageIcon
    -
    + +
    + RetentionIcon +
    + + + +
    S3TierIcon
    -
    + +
    S3TierIconXs
    -
    + +
    SearchIcon
    -
    + +
    SelectMultipleIcon
    -
    + +
    ServersIcon
    -
    + +
    ServiceAccountCredentialsIcon
    -
    + +
    ServiceAccountIcon
    -
    + +
    ServiceAccountsIcon
    -
    + +
    SettingsIcon
    -
    + +
    ShareIcon
    -
    + +
    SpeedtestIcon
    -
    + +
    StorageIcon
    -
    + +
    SyncIcon
    -
    + +
    + TagsIcon +
    + + + +
    TenantsIcon
    -
    + +
    TenantsOutlineIcon
    -
    + +
    TiersIcon
    -
    + +
    TiersNotAvailableIcon
    -
    + +
    ToolsIcon
    -
    + +
    TotalObjectsIcon
    -
    + +
    TraceIcon
    -
    + +
    TrashIcon
    -
    + +
    UploadFile
    -
    + +
    UploadFolderIcon
    -
    + +
    UploadIcon
    -
    + +
    UploadStatIcon
    -
    + +
    UptimeIcon
    -
    + +
    UsersIcon
    -
    + +
    VerifiedIcon
    -
    + +
    VersionIcon
    -
    + +
    VersionsIcon
    -
    + +
    WarnIcon
    -
    + +
    WarpIcon
    -
    + +
    WatchIcon
    @@ -869,112 +1053,134 @@ const IconsScreen = ({ classes }: IIconsScreenSimple) => { })} > -
    + +
    AccessMenuIcon
    -
    + +
    AccountsMenuIcon
    -
    + +
    AuditLogsMenuIcon
    -
    + +
    BucketsMenuIcon
    -
    + +
    CallHomeMenuIcon
    -
    + +
    DiagnosticsMenuIcon
    -
    + +
    DrivesMenuIcon
    -
    + +
    GroupsMenuIcon
    -
    + +
    HealthMenuIcon
    -
    + +
    IdentityMenuIcon
    -
    + +
    InspectMenuIcon
    -
    + +
    LogsMenuIcon
    -
    + +
    MenuCollapsedIcon
    -
    + +
    MenuExpandedIcon
    -
    + +
    MetricsMenuIcon
    -
    + +
    MonitoringMenuIcon
    -
    + +
    PerformanceMenuIcon
    -
    + +
    ProfileMenuIcon
    -
    + +
    RegisterMenuIcon
    -
    + +
    SupportMenuIcon
    -
    + +
    TraceMenuIcon
    -
    + +
    UsersMenuIcon
    diff --git a/restapi/embedded_spec.go b/restapi/embedded_spec.go index 7aaaf9830..6d1137a02 100644 --- a/restapi/embedded_spec.go +++ b/restapi/embedded_spec.go @@ -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" }, diff --git a/restapi/user_objects.go b/restapi/user_objects.go index 21d50ecd7..9327b99fe 100644 --- a/restapi/user_objects.go +++ b/restapi/user_objects.go @@ -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, "/") { diff --git a/swagger-console.yml b/swagger-console.yml index 3f64e56a4..8daf31bc4 100644 --- a/swagger-console.yml +++ b/swagger-console.yml @@ -2793,6 +2793,8 @@ definitions: type: string retention_until_date: type: string + etag: + type: string tags: type: object additionalProperties: