Implemented AGPL MinIO Object Browser simplified Console (#3509)

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2025-03-11 03:30:53 -06:00
committed by GitHub
parent 33a7fbb205
commit 63c6d8952b
718 changed files with 1287 additions and 111051 deletions

View File

@@ -1,62 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2023 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { expect } from "@playwright/test";
import { generateUUID, test } from "./fixtures/baseFixture";
import { minioadminFile } from "./consts";
import { BUCKET_LIST_PAGE } from "./consts";
test.use({ storageState: minioadminFile });
test.beforeEach(async ({ page }) => {
await page.goto(BUCKET_LIST_PAGE);
await page.waitForTimeout(1000);
});
test("create a new bucket", async ({ page }) => {
await page.getByRole("button", { name: "Buckets" }).click();
await page.getByRole("button", { name: "Create Bucket" }).click();
await page.getByLabel("Bucket Name*").click();
const bucketName = `new-bucket-${generateUUID()}`;
await page.getByLabel("Bucket Name*").fill(bucketName);
await page.getByRole("button", { name: "Create Bucket" }).click();
await page.waitForTimeout(2000);
await page.locator("#refresh-buckets").click();
await page.getByPlaceholder("Search Buckets").fill(bucketName);
await expect(page.locator(`#manageBucket-${bucketName}`)).toBeTruthy();
const bucketLocatorEl = `#manageBucket-${bucketName}`;
await page.locator(bucketLocatorEl).click();
await page.locator("#delete-bucket-button").click();
//confirm modal
await page.locator("#confirm-ok").click();
const listItemsCount = await page.locator(bucketLocatorEl).count();
await expect(listItemsCount).toEqual(0);
});
test("invalid bucket name", async ({ page }) => {
await page.getByRole("button", { name: "Buckets" }).click();
await page.getByRole("button", { name: "Create Bucket" }).click();
await page.getByLabel("Bucket Name*").click();
await page.getByLabel("Bucket Name*").fill("invalid name");
await page.getByRole("button", { name: "View Bucket Naming Rules" }).click();
await expect(
page.getByText(
"Bucket names can consist only of lowercase letters, numbers, dots (.), and hyphe",
),
).toBeTruthy();
});

View File

@@ -1,38 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2023 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { expect } from "@playwright/test";
import { generateUUID, test } from "./fixtures/baseFixture";
import { minioadminFile } from "./consts";
import { BUCKET_LIST_PAGE } from "./consts";
test.use({ storageState: minioadminFile });
test.beforeEach(async ({ page }) => {
await page.goto(BUCKET_LIST_PAGE);
});
test("Add a new group", async ({ page }) => {
await page.getByRole("button", { name: "Identity" }).click();
await page.getByRole("button", { name: "Groups" }).click();
await page.getByRole("button", { name: "Create Group" }).click();
const groupName = `new-group-${generateUUID()}`;
await page.getByLabel("Group Name").fill(groupName);
await page.getByRole("button", { name: "Save" }).click();
await expect(page.getByRole("gridcell", { name: groupName })).toBeTruthy();
});

View File

@@ -1,46 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2023 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { expect } from "@playwright/test";
import { generateUUID, test } from "./fixtures/baseFixture";
import { minioadminFile } from "./consts";
import { BUCKET_LIST_PAGE } from "./consts";
test.use({ storageState: minioadminFile });
test.beforeEach(async ({ page }) => {
await page.goto(BUCKET_LIST_PAGE);
});
test("Can create a policy", async ({ page }) => {
await page.getByRole("button", { name: "Policies" }).click();
await page.getByRole("button", { name: "Create Policy" }).click();
await page.getByLabel("Policy Name").click();
const policyName = `policy-${generateUUID()}`;
await page.getByLabel("Policy Name").fill(policyName);
await page.locator("#code_wrapper").click();
await page.locator("#code_wrapper").click();
await page.locator("#code_wrapper").click();
await page.locator("#code_wrapper").press("Meta+a");
await page
.locator("#code_wrapper")
.fill(
'{\n "Version": "2012-10-17",\n "Statement": [\n {\n "Effect": "Allow",\n "Action": [\n "s3:*"\n ],\n "Resource": [\n "arn:aws:s3:::bucket1/*",\n "arn:aws:s3:::bucket2/*",\n "arn:aws:s3:::bucket3/*",\n "arn:aws:s3:::bucket4/*"\n ]\n },\n {\n "Effect": "Deny",\n "Action": [\n "s3:DeleteBucket"\n ],\n "Resource": [\n "arn:aws:s3:::*"\n ]\n }\n ]\n}\n',
);
await page.getByRole("button", { name: "Save" }).click();
await expect(page.getByRole("gridcell", { name: policyName })).toBeTruthy();
});

View File

@@ -12,9 +12,8 @@
"local-storage-fallback": "^4.1.3",
"lodash": "^4.17.21",
"luxon": "^3.5.0",
"mds": "https://github.com/minio/mds.git#v1.0.4",
"mds": "https://github.com/minio/mds.git#v1.1.1",
"react": "^18.3.1",
"react-component-export-image": "^1.0.6",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18.3.1",
"react-dropzone": "^14.3.5",
@@ -25,7 +24,6 @@
"react-virtualized": "^9.22.6",
"react-window": "^1.8.11",
"react-window-infinite-loader": "^1.0.10",
"recharts": "^2.15.1",
"styled-components": "5.3.11",
"superagent": "^9.0.2",
"tinycolor2": "^1.6.0"

File diff suppressed because it is too large Load Diff

View File

@@ -1,192 +0,0 @@
// 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 "../../../utils/matchMedia";
import hasPermission from "../accessControl";
import { store } from "../../../store";
import { IAM_PAGES, IAM_PAGES_PERMISSIONS, IAM_SCOPES } from "../permissions";
import { saveSessionResponse } from "../../../screens/Console/consoleSlice";
const setPolicy1 = () => {
store.dispatch(
saveSessionResponse({
distributedMode: true,
features: ["log-search"],
permissions: {
"arn:aws:s3:::testcafe": [
"admin:CreateUser",
"s3:GetBucketLocation",
"s3:ListBucket",
"admin:CreateServiceAccount",
],
"arn:aws:s3:::testcafe/*": [
"admin:CreateServiceAccount",
"admin:CreateUser",
"s3:GetObject",
"s3:ListBucket",
],
"arn:aws:s3:::testcafe/write/*": [
"admin:CreateServiceAccount",
"admin:CreateUser",
"s3:PutObject",
"s3:DeleteObject",
"s3:GetObject",
"s3:ListBucket",
],
"console-ui": ["admin:CreateServiceAccount", "admin:CreateUser"],
},
status: "ok",
}),
);
};
const setPolicy2 = () => {
store.dispatch(
saveSessionResponse({
distributedMode: true,
features: [],
permissions: {
"arn:aws:s3:::bucket-svc": [
"admin:CreateServiceAccount",
"s3:GetBucketLocation",
"s3:ListBucket",
"s3:ListBucketMultipartUploads",
"s3:ListMultipartUploadParts",
"admin:CreateUser",
],
"arn:aws:s3:::bucket-svc/prefix1/*": [
"admin:CreateUser",
"admin:CreateServiceAccount",
"s3:GetObject",
"s3:PutObject",
],
"arn:aws:s3:::bucket-svc/prefix1/ini*": [
"admin:CreateServiceAccount",
"s3:*",
"admin:CreateUser",
],
"arn:aws:s3:::bucket-svc/prefix1/jars*": [
"admin:CreateUser",
"admin:CreateServiceAccount",
"s3:*",
],
"arn:aws:s3:::bucket-svc/prefix1/logs*": [
"admin:CreateUser",
"admin:CreateServiceAccount",
"s3:*",
],
"console-ui": ["admin:CreateServiceAccount", "admin:CreateUser"],
},
status: "ok",
}),
);
};
const setPolicy3 = () => {
store.dispatch(
saveSessionResponse({
distributedMode: true,
features: [],
permissions: {
"arn:aws:s3:::testbucket-*": [
"admin:CreateServiceAccount",
"s3:*",
"admin:CreateUser",
],
"console-ui": ["admin:CreateServiceAccount", "admin:CreateUser"],
},
status: "ok",
}),
);
};
const setPolicy4 = () => {
store.dispatch(
saveSessionResponse({
distributedMode: true,
features: [],
permissions: {
"arn:aws:s3:::test/*": ["s3:ListBucket"],
"arn:aws:s3:::test": ["s3:GetBucketLocation"],
"arn:aws:s3:::test/digitalinsights/xref_cust_guid_actd*": ["s3:*"],
},
status: "ok",
}),
);
};
test("Upload button disabled", () => {
setPolicy1();
expect(hasPermission("testcafe", ["s3:PutObject"])).toBe(false);
});
test("Upload button enabled valid prefix", () => {
setPolicy1();
expect(hasPermission("testcafe/write", ["s3:PutObject"], false, true)).toBe(
true,
);
});
test("Can Browse Bucket", () => {
setPolicy2();
expect(
hasPermission(
"bucket-svc",
IAM_PAGES_PERMISSIONS[IAM_PAGES.OBJECT_BROWSER_VIEW],
),
).toBe(true);
});
test("Can List Objects In Bucket", () => {
setPolicy2();
expect(hasPermission("bucket-svc", [IAM_SCOPES.S3_LIST_BUCKET])).toBe(true);
});
test("Can create bucket for policy with a wildcard", () => {
setPolicy3();
expect(hasPermission("*", [IAM_SCOPES.S3_CREATE_BUCKET])).toBe(true);
});
test("Can browse a bucket for a policy with a wildcard", () => {
setPolicy3();
expect(
hasPermission(
"testbucket-0",
IAM_PAGES_PERMISSIONS[IAM_PAGES.OBJECT_BROWSER_VIEW],
),
).toBe(true);
});
test("Can delete an object inside a bucket prefix", () => {
setPolicy4();
expect(
hasPermission(
[
"xref_cust_guid_actd-v1.jpg",
"test/digitalinsights/xref_cust_guid_actd-v1.jpg",
],
[IAM_SCOPES.S3_DELETE_OBJECT, IAM_SCOPES.S3_DELETE_ACTIONS],
),
).toBe(true);
});
test("Can't delete an object inside a bucket prefix", () => {
setPolicy4();
expect(
hasPermission(
["xref_cust_guid_actd-v1.jpg", "test/xref_cust_guid_actd-v1.jpg"],
[IAM_SCOPES.S3_DELETE_OBJECT, IAM_SCOPES.S3_DELETE_ACTIONS],
),
).toBe(false);
});

View File

@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
export const IAM_ROLES = {
const IAM_ROLES = {
BUCKET_OWNER: "BUCKET_OWNER", // upload/delete objects from the bucket
BUCKET_VIEWER: "BUCKET_VIEWER", // only view objects on the bucket
BUCKET_ADMIN: "BUCKET_ADMIN", // administrate the bucket
@@ -193,7 +193,7 @@ export const IAM_PAGES = {
};
// roles
export const IAM_PERMISSIONS = {
const IAM_PERMISSIONS = {
[IAM_ROLES.BUCKET_OWNER]: [
IAM_SCOPES.S3_PUT_OBJECT,
IAM_SCOPES.S3_PUT_ACTIONS,
@@ -397,7 +397,6 @@ export const IAM_PAGES_PERMISSIONS = {
],
};
export const S3_ALL_RESOURCES = "arn:aws:s3:::*";
export const CONSOLE_UI_RESOURCE = "console-ui";
export const permissionTooltipHelper = (scopes: string[], name: string) => {
@@ -415,106 +414,3 @@ export const permissionTooltipHelper = (scopes: string[], name: string) => {
"."
);
};
export const listUsersPermissions = [IAM_SCOPES.ADMIN_LIST_USERS];
export const addUserToGroupPermissions = [IAM_SCOPES.ADMIN_ADD_USER_TO_GROUP];
export const deleteUserPermissions = [IAM_SCOPES.ADMIN_DELETE_USER];
export const enableUserPermissions = [IAM_SCOPES.ADMIN_ENABLE_USER];
export const disableUserPermissions = [IAM_SCOPES.ADMIN_DISABLE_USER];
//note that adminUserPermissions does NOT include ADMIN_CREATE_USER to allow hiding the Users tab for users wtih only this permission as it is being applied by default
export const adminUserPermissions = [
IAM_SCOPES.ADMIN_LIST_USER_POLICIES,
IAM_SCOPES.ADMIN_LIST_USERS,
IAM_SCOPES.ADMIN_ADD_USER_TO_GROUP,
IAM_SCOPES.ADMIN_REMOVE_USER_FROM_GROUP,
IAM_SCOPES.ADMIN_ATTACH_USER_OR_GROUP_POLICY,
IAM_SCOPES.ADMIN_LIST_USERS,
IAM_SCOPES.ADMIN_DELETE_USER,
IAM_SCOPES.ADMIN_ENABLE_USER,
IAM_SCOPES.ADMIN_DISABLE_USER,
IAM_SCOPES.ADMIN_GET_USER,
IAM_SCOPES.ADMIN_LIST_USER_POLICIES,
];
export const assignIAMPolicyPermissions = [
IAM_SCOPES.ADMIN_ATTACH_USER_OR_GROUP_POLICY,
IAM_SCOPES.ADMIN_LIST_USER_POLICIES,
IAM_SCOPES.ADMIN_GET_POLICY,
];
export const assignGroupPermissions = [
IAM_SCOPES.ADMIN_ADD_USER_TO_GROUP,
IAM_SCOPES.ADMIN_REMOVE_USER_FROM_GROUP,
IAM_SCOPES.ADMIN_LIST_GROUPS,
IAM_SCOPES.ADMIN_ENABLE_USER,
];
export const getGroupPermissions = [IAM_SCOPES.ADMIN_GET_GROUP];
export const enableDisableUserPermissions = [
IAM_SCOPES.ADMIN_ENABLE_USER,
IAM_SCOPES.ADMIN_DISABLE_USER,
];
export const editServiceAccountPermissions = [
IAM_SCOPES.ADMIN_LIST_SERVICEACCOUNTS,
IAM_SCOPES.ADMIN_UPDATE_SERVICEACCOUNT,
IAM_SCOPES.ADMIN_REMOVE_SERVICEACCOUNT,
];
export const applyPolicyPermissions = [
IAM_SCOPES.ADMIN_ATTACH_USER_OR_GROUP_POLICY,
IAM_SCOPES.ADMIN_LIST_USER_POLICIES,
];
export const deleteGroupPermissions = [IAM_SCOPES.ADMIN_REMOVE_USER_FROM_GROUP];
export const displayGroupsPermissions = [IAM_SCOPES.ADMIN_LIST_GROUPS];
export const createGroupPermissions = [
IAM_SCOPES.ADMIN_ADD_USER_TO_GROUP,
IAM_SCOPES.ADMIN_LIST_USERS,
];
export const viewUserPermissions = [
IAM_SCOPES.ADMIN_GET_USER,
IAM_SCOPES.ADMIN_LIST_USERS,
];
export const editGroupMembersPermissions = [
IAM_SCOPES.ADMIN_ADD_USER_TO_GROUP,
IAM_SCOPES.ADMIN_LIST_USERS,
];
export const setGroupPoliciesPermissions = [
IAM_SCOPES.ADMIN_ATTACH_USER_OR_GROUP_POLICY,
IAM_SCOPES.ADMIN_LIST_USER_POLICIES,
];
export const viewPolicyPermissions = [IAM_SCOPES.ADMIN_GET_POLICY];
export const enableDisableGroupPermissions = [
IAM_SCOPES.ADMIN_ENABLE_GROUP,
IAM_SCOPES.ADMIN_DISABLE_GROUP,
];
export const createPolicyPermissions = [IAM_SCOPES.ADMIN_CREATE_POLICY];
export const deletePolicyPermissions = [IAM_SCOPES.ADMIN_DELETE_POLICY];
export const listPolicyPermissions = [IAM_SCOPES.ADMIN_LIST_USER_POLICIES];
export const listGroupPermissions = [
IAM_SCOPES.ADMIN_LIST_GROUPS,
IAM_SCOPES.ADMIN_GET_GROUP,
];
export const deleteBucketPermissions = [
IAM_SCOPES.S3_DELETE_BUCKET,
IAM_SCOPES.S3_FORCE_DELETE_BUCKET,
];
export const browseBucketPermissions = [
IAM_SCOPES.S3_LIST_BUCKET,
IAM_SCOPES.S3_ALL_LIST_BUCKET,
];

View File

@@ -36,11 +36,6 @@ export interface ErrorResponseHandler {
statusCode?: number;
}
export interface IBytesCalc {
total: number;
unit: string;
}
interface IEmbeddedCustomButton {
backgroundColor: string;
textColor: string;
@@ -97,8 +92,3 @@ export interface IEmbeddedCustomStyles {
inputBox: IEmbeddedInputBox;
switch: IEmbeddedSwitch;
}
export interface SelectorTypes {
label: any;
value: string;
}

View File

@@ -15,23 +15,13 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import storage from "local-storage-fallback";
import { IBytesCalc, IErasureCodeCalc, IStorageFactors } from "./types";
import { IErasureCodeCalc, IStorageFactors } from "./types";
import get from "lodash/get";
const minMemReq = 2147483648; // Minimal Memory required for MinIO in bytes
export const units = [
"B",
"KiB",
"MiB",
"GiB",
"TiB",
"PiB",
"EiB",
"ZiB",
"YiB",
];
const units = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
const k8sUnits = ["Ki", "Mi", "Gi", "Ti", "Pi", "Ei"];
const k8sCalcUnits = ["B", ...k8sUnits];
@@ -53,7 +43,7 @@ export const niceBytesInt = (n: number, showK8sUnits: boolean = false) => {
return n.toFixed(1) + " " + (showK8sUnits ? k8sUnitsN[l] : units[l]);
};
export const deleteCookie = (name: string) => {
const deleteCookie = (name: string) => {
document.cookie = name + "=; expires=Thu, 01 Jan 1970 00:00:01 GMT;";
};
@@ -64,20 +54,6 @@ export const clearSession = () => {
deleteCookie("idp-refresh-token");
};
// units to be used in a dropdown
export const k8sScalarUnitsExcluding = (exclude?: string[]) => {
return k8sUnits
.filter((unit) => {
if (exclude && exclude.includes(unit)) {
return false;
}
return true;
})
.map((unit) => {
return { label: unit, value: unit };
});
};
//getBytes, converts from a value and a unit from units array to bytes as a string
export const getBytes = (
value: string,
@@ -272,13 +248,6 @@ export const niceTimeFromSeconds = (seconds: number): string => {
return parts.join(" and ");
};
// seconds / minutes /hours / Days / Years calculator
export const niceDays = (secondsValue: string, timeVariant: string = "s") => {
let seconds = parseFloat(secondsValue);
return niceDaysInt(seconds, timeVariant);
};
// niceDaysInt returns the string in the max unit found e.g. 92400 seconds -> 1 day
export const niceDaysInt = (seconds: number, timeVariant: string = "s") => {
switch (timeVariant) {
@@ -334,159 +303,6 @@ export const niceDaysInt = (seconds: number, timeVariant: string = "s") => {
}`;
};
const twoDigitsNumberString = (value: number) => {
return `${value < 10 ? "0" : ""}${value}`;
};
export const getTimeFromTimestamp = (
timestamp: string,
fullDate: boolean = false,
simplifiedDate: boolean = false,
) => {
const timestampToInt = parseInt(timestamp);
if (isNaN(timestampToInt)) {
return "";
}
const dateObject = new Date(timestampToInt * 1000);
if (fullDate) {
if (simplifiedDate) {
return `${twoDigitsNumberString(
dateObject.getMonth() + 1,
)}/${twoDigitsNumberString(dateObject.getDate())} ${twoDigitsNumberString(
dateObject.getHours(),
)}:${twoDigitsNumberString(dateObject.getMinutes())}`;
} else {
return dateObject.toLocaleString();
}
}
return `${dateObject.getHours()}:${String(dateObject.getMinutes()).padStart(
2,
"0",
)}`;
};
export const calculateBytes = (
x: string | number,
showDecimals = false,
roundFloor = true,
k8sUnit = false,
): IBytesCalc => {
let bytes;
if (typeof x === "string") {
bytes = parseInt(x, 10);
} else {
bytes = x;
}
if (bytes === 0) {
return { total: 0, unit: units[0] };
}
// Gi : GiB
const k = 1024;
// Get unit for measure
const i = Math.floor(Math.log(bytes) / Math.log(k));
const fractionDigits = showDecimals ? 1 : 0;
const bytesUnit = bytes / Math.pow(k, i);
const roundedUnit = roundFloor ? Math.floor(bytesUnit) : bytesUnit;
// Get Unit parsed
const unitParsed = parseFloat(roundedUnit.toFixed(fractionDigits));
const finalUnit = k8sUnit ? k8sCalcUnits[i] : units[i];
return { total: unitParsed, unit: finalUnit };
};
export const nsToSeconds = (nanoseconds: number) => {
const conversion = nanoseconds * 0.000000001;
const round = Math.round((conversion + Number.EPSILON) * 10000) / 10000;
return `${round} s`;
};
export const textToRGBColor = (text: string) => {
const splitText = text.split("");
const hashVl = splitText.reduce((acc, currItem) => {
return acc + currItem.charCodeAt(0) + ((acc << 5) - acc);
}, 0);
const hashColored = ((hashVl * 100) & 0x00ffffff).toString(16).toUpperCase();
return `#${hashColored.padStart(6, "0")}`;
};
export const prettyNumber = (usage: number | undefined) => {
if (usage === undefined) {
return 0;
}
return usage.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
export const representationNumber = (number: number | undefined) => {
if (number === undefined) {
return "0";
}
let returnValue = number.toString();
let unit = "";
if (number > 999 && number < 1000000) {
returnValue = (number / 1000).toFixed(1); // convert to K, numbers > 999
unit = "K";
} else if (number >= 1000000 && number < 1000000000) {
returnValue = (number / 1000000).toFixed(1); // convert to M, numbers >= 1 million
unit = "M";
} else if (number >= 1000000000) {
returnValue = (number / 1000000000).toFixed(1); // convert to B, numbers >= 1 billion
unit = "B";
}
if (returnValue.endsWith(".0")) {
returnValue = returnValue.slice(0, -2);
}
return `${returnValue}${unit}`;
};
/** Ref https://developer.mozilla.org/en-US/docs/Glossary/Base64 */
export const performDownload = (blob: Blob, fileName: string) => {
const link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
export const getCookieValue = (cookieName: string) => {
return (
document.cookie
.match("(^|;)\\s*" + cookieName + "\\s*=\\s*([^;]+)")
?.pop() || ""
);
};
export const capacityColors = (usedSpace: number, maxSpace: number) => {
const percCalculate = (usedSpace * 100) / maxSpace;
if (percCalculate >= 90) {
return "#C83B51";
} else if (percCalculate >= 70) {
return "#FFAB0F";
}
return "#07193E";
};
export const getClientOS = (): string => {
const getPlatform = get(window.navigator, "platform", "undefined");
@@ -497,18 +313,6 @@ export const getClientOS = (): string => {
return getPlatform;
};
// Generates a valid access/secret key string
export const getRandomString = function (length = 16): string {
let retval = "";
let legalcharacters =
"1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
for (let i = 0; i < length; i++) {
retval +=
legalcharacters[Math.floor(Math.random() * legalcharacters.length)];
}
return retval;
};
// replaces bad unicode characters
export const replaceUnicodeChar = (inputString: string): string => {
let unicodeChar = "\u202E";

View File

@@ -65,8 +65,3 @@ export const getLogoApplicationVariant =
return "console";
}
};
export const registeredCluster = (): boolean => {
const plan = getLogoVar();
return ["standard", "enterprise", "enterpriseos"].includes(plan || "AGPL");
};

View File

@@ -1,37 +0,0 @@
// 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 EncryptionIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="255.209"
height="255.209"
viewBox="0 0 255.209 255.209"
className={`min-icon`}
fill={"currentcolor"}
{...props}
>
<path
id="KMS"
d="M175.664,255.209V228.695H79.546v26.515H46.4V228.695H3a3,3,0,0,1-3-3V3A3,3,0,0,1,3,0H252.21a3,3,0,0,1,3,3V225.694a3,3,0,0,1-3,3h-43.4v26.515ZM23.2,29.83V198.865a9.954,9.954,0,0,0,9.943,9.943H222.065a9.954,9.954,0,0,0,9.943-9.943V29.83a9.954,9.954,0,0,0-9.943-9.943H33.144A9.954,9.954,0,0,0,23.2,29.83ZM222.065,198.866h0Zm-188.921,0V29.83H222.065V198.865H33.144ZM69.224,88.258a26.52,26.52,0,1,0,34.909,34.375h33.071a2,2,0,0,0,2-2V104.747a2,2,0,0,0-2-2H104.134A26.545,26.545,0,0,0,69.224,88.258ZM59.659,112.69a19.886,19.886,0,1,1,19.886,19.886A19.887,19.887,0,0,1,59.659,112.69Z"
/>
</svg>
);
export default EncryptionIcon;

View File

@@ -1,38 +0,0 @@
// 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 EncryptionStatusIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="256"
height="162.281"
viewBox="0 0 256 162.281"
className={`min-icon`}
fill={"currentcolor"}
{...props}
>
<path
id="KMS-status"
d="M-13110.45-17976.135a8.3,8.3,0,0,1-7.6-4.979l-30.661-70.426h-41.776a8.3,8.3,0,0,1-8.292-8.3,8.3,8.3,0,0,1,8.292-8.3h47.211a8.289,8.289,0,0,1,7.6,4.98l23.306,53.533,32.412-122.619a8.3,8.3,0,0,1,8.017-6.178h.074a8.293,8.293,0,0,1,7.978,6.336l23.061,94.307,25.367-45.307a8.267,8.267,0,0,1,7.232-4.254c.136,0,.276,0,.416.01a8.315,8.315,0,0,1,7.189,4.979l20.733,47.732h28.818a8.292,8.292,0,0,1,8.293,8.287,8.294,8.294,0,0,1-8.293,8.3h-34.254a8.273,8.273,0,0,1-7.6-4.988l-16.239-37.379-27.48,49.107a8.274,8.274,0,0,1-7.233,4.244,9.94,9.94,0,0,1-1.12-.07,8.309,8.309,0,0,1-6.936-6.258l-20.317-83.1-30.171,114.166a8.3,8.3,0,0,1-7.387,6.152C-13110.021-17976.143-13110.24-17976.135-13110.45-17976.135Z"
transform="translate(13198.776 18138.416)"
/>
</svg>
);
export default EncryptionStatusIcon;

View File

@@ -1,326 +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 {
AccountIcon,
AddIcon,
Box,
Button,
DataTable,
DeleteIcon,
Grid,
HelpBox,
PageLayout,
PasswordKeyIcon,
} from "mds";
import { useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { actionsTray } from "../Common/FormComponents/common/styleLibrary";
import ChangePasswordModal from "./ChangePasswordModal";
import SearchBox from "../Common/SearchBox";
import withSuspense from "../Common/Components/withSuspense";
import { selectSAs } from "../Configurations/utils";
import DeleteMultipleServiceAccounts from "../Users/DeleteMultipleServiceAccounts";
import EditServiceAccount from "./EditServiceAccount";
import { selFeatures } from "../consoleSlice";
import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper";
import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper";
import { api } from "api";
import { errorToHandler } from "api/errors";
import HelpMenu from "../HelpMenu";
import { ACCOUNT_TABLE_COLUMNS } from "./AccountUtils";
import { useAppDispatch } from "store";
import { ServiceAccounts } from "api/consoleApi";
import {
setErrorSnackMessage,
setHelpName,
setSnackBarMessage,
} from "systemSlice";
import { usersSort } from "utils/sortFunctions";
import { SecureComponent } from "common/SecureComponent";
import {
CONSOLE_UI_RESOURCE,
IAM_PAGES,
IAM_SCOPES,
} from "common/SecureComponent/permissions";
const DeleteServiceAccount = withSuspense(
React.lazy(() => import("./DeleteServiceAccount")),
);
const Account = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const features = useSelector(selFeatures);
const [records, setRecords] = useState<ServiceAccounts>([]);
const [loading, setLoading] = useState<boolean>(false);
const [filter, setFilter] = useState<string>("");
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [selectedServiceAccount, setSelectedServiceAccount] = useState<
string | null
>(null);
const [changePasswordModalOpen, setChangePasswordModalOpen] =
useState<boolean>(false);
const [selectedSAs, setSelectedSAs] = useState<string[]>([]);
const [deleteMultipleOpen, setDeleteMultipleOpen] = useState<boolean>(false);
const [isEditOpen, setIsEditOpen] = useState<boolean>(false);
const userIDP = (features && features.includes("external-idp")) || false;
useEffect(() => {
fetchRecords();
}, []);
useEffect(() => {
dispatch(setHelpName("accessKeys"));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (loading) {
api.serviceAccounts
.listUserServiceAccounts()
.then((res) => {
setLoading(false);
const sortedRows = res.data.sort(usersSort);
setRecords(sortedRows);
})
.catch((res) => {
dispatch(
setErrorSnackMessage(
errorToHandler(res?.error || "Error retrieving access keys"),
),
);
setLoading(false);
});
}
}, [loading, setLoading, setRecords, dispatch]);
const fetchRecords = () => {
setLoading(true);
};
const closeDeleteModalAndRefresh = (refresh: boolean) => {
setDeleteOpen(false);
if (refresh) {
setSelectedSAs([]);
fetchRecords();
}
};
const closeDeleteMultipleModalAndRefresh = (refresh: boolean) => {
setDeleteMultipleOpen(false);
if (refresh) {
dispatch(setSnackBarMessage(`Access keys deleted successfully.`));
setSelectedSAs([]);
setLoading(true);
}
};
const editModalOpen = (selectedServiceAccount: string) => {
setSelectedServiceAccount(selectedServiceAccount);
setIsEditOpen(true);
};
const closePolicyModal = () => {
setIsEditOpen(false);
setLoading(true);
};
const confirmDeleteServiceAccount = (selectedServiceAccount: string) => {
setSelectedServiceAccount(selectedServiceAccount);
setDeleteOpen(true);
};
const tableActions = [
{
type: "view",
onClick: (value: any) => {
if (value) {
editModalOpen(value.accessKey);
}
},
},
{
type: "delete",
onClick: (value: any) => {
if (value) {
confirmDeleteServiceAccount(value.accessKey);
}
},
},
{
type: "edit",
onClick: (value: any) => {
if (value) {
editModalOpen(value.accessKey);
}
},
},
];
const filteredRecords = records.filter((elementItem) =>
elementItem?.accessKey?.toLowerCase().includes(filter.toLowerCase()),
);
return (
<React.Fragment>
{deleteOpen && (
<DeleteServiceAccount
deleteOpen={deleteOpen}
selectedServiceAccount={selectedServiceAccount}
closeDeleteModalAndRefresh={(refresh: boolean) => {
closeDeleteModalAndRefresh(refresh);
}}
/>
)}
{deleteMultipleOpen && (
<DeleteMultipleServiceAccounts
deleteOpen={deleteMultipleOpen}
selectedSAs={selectedSAs}
closeDeleteModalAndRefresh={closeDeleteMultipleModalAndRefresh}
/>
)}
{isEditOpen && (
<EditServiceAccount
open={isEditOpen}
selectedAccessKey={selectedServiceAccount}
closeModalAndRefresh={closePolicyModal}
/>
)}
<ChangePasswordModal
open={changePasswordModalOpen}
closeModal={() => setChangePasswordModalOpen(false)}
/>
<PageHeaderWrapper label="Access Keys" actions={<HelpMenu />} />
<PageLayout>
<Grid container>
<Grid item xs={12} sx={{ ...actionsTray.actionsTray }}>
<SearchBox
placeholder={"Search Access Keys"}
onChange={setFilter}
sx={{ marginRight: "auto", maxWidth: 380 }}
value={filter}
/>
<Box
sx={{
display: "flex",
flexWrap: "nowrap",
gap: 5,
}}
>
<TooltipWrapper tooltip={"Delete Selected"}>
<Button
id={"delete-selected-accounts"}
onClick={() => {
setDeleteMultipleOpen(true);
}}
label={"Delete Selected"}
icon={<DeleteIcon />}
disabled={selectedSAs.length === 0}
variant={"secondary"}
/>
</TooltipWrapper>
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_CREATE_USER]}
resource={CONSOLE_UI_RESOURCE}
matchAll
errorProps={{ disabled: true }}
>
<Button
id={"change-password"}
onClick={() => setChangePasswordModalOpen(true)}
label={`Change Password`}
icon={<PasswordKeyIcon />}
variant={"regular"}
disabled={userIDP}
/>
</SecureComponent>
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_CREATE_SERVICEACCOUNT]}
resource={CONSOLE_UI_RESOURCE}
matchAll
errorProps={{ disabled: true }}
>
<Button
id={"create-service-account"}
onClick={() => {
navigate(`${IAM_PAGES.ACCOUNT_ADD}`);
}}
label={`Create access key`}
icon={<AddIcon />}
variant={"callAction"}
/>
</SecureComponent>
</Box>
</Grid>
<Grid item xs={12}>
<DataTable
itemActions={tableActions}
entityName={"Access Keys"}
columns={ACCOUNT_TABLE_COLUMNS}
onSelect={(e) => selectSAs(e, setSelectedSAs, selectedSAs)}
selectedItems={selectedSAs}
isLoading={loading}
records={filteredRecords}
idField="accessKey"
/>
</Grid>
<Grid item xs={12} sx={{ marginTop: 15 }}>
<HelpBox
title={"Learn more about ACCESS KEYS"}
iconComponent={<AccountIcon />}
help={
<Fragment>
MinIO access keys are child identities of an authenticated
MinIO user, including externally managed identities. Each
access key inherits its privileges based on the policies
attached to its parent user or those groups in which the
parent user has membership. Access Keys also support an
optional inline policy which further restricts access to a
subset of actions and resources available to the parent user.
<br />
<br />
You can learn more at our{" "}
<a
href="https://min.io/docs/minio/linux/administration/identity-access-management/minio-user-management.html?ref=con#id3"
target="_blank"
rel="noopener"
>
documentation
</a>
.
</Fragment>
}
/>
</Grid>
</Grid>
</PageLayout>
</React.Fragment>
);
};
export default Account;

View File

@@ -1,50 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2023 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { DateTime } from "luxon";
export const ACCOUNT_TABLE_COLUMNS = [
{ label: "Access Key", elementKey: "accessKey" },
{
label: "Expiry",
elementKey: "expiration",
renderFunction: (expTime: string) => {
if (expTime !== "1970-01-01T00:00:00Z") {
const fmtDate = DateTime.fromISO(expTime)
.toUTC()
.toFormat("y/M/d hh:mm:ss z");
return <span title={fmtDate}>{fmtDate}</span>;
} else {
return <span>no-expiry</span>;
}
},
},
{
label: "Status",
elementKey: "accountStatus",
renderFunction: (status: string) => {
if (status === "off") {
return "Disabled";
} else {
return "Enabled";
}
},
},
{ label: "Name", elementKey: "name" },
{ label: "Description", elementKey: "description" },
];

View File

@@ -1,139 +0,0 @@
// 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 from "react";
import {
Box,
HelpIconFilled,
IAMPoliciesIcon,
PasswordKeyIcon,
ServiceAccountIcon,
} from "mds";
const FeatureItem = ({
icon,
description,
}: {
icon: any;
description: string;
}) => {
return (
<Box
sx={{
display: "flex",
"& .min-icon": {
marginRight: "10px",
height: "23px",
width: "23px",
marginBottom: "10px",
},
}}
>
{icon}{" "}
<div style={{ fontSize: "14px", fontStyle: "italic", color: "#5E5E5E" }}>
{description}
</div>
</Box>
);
};
const AddServiceAccountHelpBox = () => {
return (
<Box
sx={{
flex: 1,
border: "1px solid #eaeaea",
borderRadius: "2px",
display: "flex",
flexFlow: "column",
padding: "20px",
marginTop: 0,
}}
>
<Box
sx={{
fontSize: "16px",
fontWeight: 600,
display: "flex",
alignItems: "center",
marginBottom: "16px",
paddingBottom: "20px",
"& .min-icon": {
height: "21px",
width: "21px",
marginRight: "15px",
},
}}
>
<HelpIconFilled />
<div>Learn more about Access Keys</div>
</Box>
<Box sx={{ fontSize: "14px", marginBottom: "15px" }}>
<Box sx={{ paddingBottom: "20px" }}>
<FeatureItem
icon={<ServiceAccountIcon />}
description={`Create Access Keys`}
/>
<Box sx={{ paddingTop: "20px" }}>
Access Keys inherit the policies explicitly attached to the parent
user, and the policies attached to each group in which the parent
user has membership.
</Box>
</Box>
<Box sx={{ paddingBottom: "20px" }}>
<FeatureItem
icon={<PasswordKeyIcon />}
description={`Assign Custom Credentials`}
/>
<Box sx={{ paddingTop: "10px" }}>
Randomized access credentials are recommended, and provided by
default. You may use your own custom Access Key and Secret Key by
replacing the default values. After creation of any Access Key, you
will be given the opportunity to view and download the account
credentials.
</Box>
<Box sx={{ paddingTop: "10px" }}>
Access Keys support programmatic access by applications. You cannot
use a Access Key to log into the MinIO Console.
</Box>
</Box>
<Box sx={{ paddingBottom: "20px" }}>
<FeatureItem
icon={<IAMPoliciesIcon />}
description={`Assign Access Policies`}
/>
<Box sx={{ paddingTop: "10px" }}>
You can specify an optional JSON-formatted IAM policy to further
restrict Access Key access to a subset of the actions and resources
explicitly allowed for the parent user. Additional access beyond
that of the parent user cannot be implemented through these
policies.
</Box>
<Box sx={{ paddingTop: "10px" }}>
You cannot modify the optional Access Key IAM policy after saving.
</Box>
</Box>
</Box>
<Box
sx={{
display: "flex",
flexFlow: "column",
}}
></Box>
</Box>
);
};
export default AddServiceAccountHelpBox;

View File

@@ -1,338 +0,0 @@
// 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, { Fragment, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import {
BackLink,
Button,
PageLayout,
PasswordKeyIcon,
ServiceAccountCredentialsIcon,
Grid,
Box,
FormLayout,
InputBox,
Switch,
ServiceAccountIcon,
HelpTip,
DateTimeInput,
} from "mds";
import { modalStyleUtils } from "../Common/FormComponents/common/styleLibrary";
import { NewServiceAccount } from "../Common/CredentialsPrompt/types";
import { IAM_PAGES } from "../../../common/SecureComponent/permissions";
import { setErrorSnackMessage, setHelpName } from "../../../systemSlice";
import { api } from "api";
import { errorToHandler } from "api/errors";
import { ContentType } from "api/consoleApi";
import CodeMirrorWrapper from "../Common/FormComponents/CodeMirrorWrapper/CodeMirrorWrapper";
import AddServiceAccountHelpBox from "./AddServiceAccountHelpBox";
import CredentialsPrompt from "../Common/CredentialsPrompt/CredentialsPrompt";
import PanelTitle from "../Common/PanelTitle/PanelTitle";
import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper";
import HelpMenu from "../HelpMenu";
import { useAppDispatch } from "store";
import { getRandomString } from "common/utils";
const AddServiceAccount = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const [addSending, setAddSending] = useState<boolean>(false);
const [accessKey, setAccessKey] = useState<string>(getRandomString(20));
const [secretKey, setSecretKey] = useState<string>(getRandomString(40));
const [isRestrictedByPolicy, setIsRestrictedByPolicy] =
useState<boolean>(false);
const [newServiceAccount, setNewServiceAccount] =
useState<NewServiceAccount | null>(null);
const [policyJSON, setPolicyJSON] = useState<string>("");
const [name, setName] = useState<string>("");
const [description, setDescription] = useState<string>("");
const [comments, setComments] = useState<string>("");
const [expiry, setExpiry] = useState<any>();
useEffect(() => {
dispatch(setHelpName("add_service_account"));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (addSending) {
const expiryDt = expiry ? expiry.toJSDate().toISOString() : null;
api.serviceAccountCredentials
.createServiceAccountCreds(
{
policy: policyJSON,
accessKey: accessKey,
secretKey: secretKey,
description: description,
comment: comments,
name: name,
expiry: expiryDt,
},
{ type: ContentType.Json },
)
.then((res) => {
setAddSending(false);
setNewServiceAccount({
accessKey: res.data.accessKey || "",
secretKey: res.data.secretKey || "",
url: res.url || "",
});
})
.catch((res) => {
setAddSending(false);
dispatch(setErrorSnackMessage(errorToHandler(res.error)));
});
}
}, [
addSending,
setAddSending,
dispatch,
policyJSON,
accessKey,
secretKey,
name,
description,
expiry,
comments,
]);
useEffect(() => {
if (isRestrictedByPolicy) {
api.user.getUserPolicy().then((res) => {
setPolicyJSON(JSON.stringify(JSON.parse(res.data), null, 4));
});
}
}, [isRestrictedByPolicy]);
const addServiceAccount = (e: React.FormEvent) => {
e.preventDefault();
setAddSending(true);
};
const resetForm = () => {
setPolicyJSON("");
setNewServiceAccount(null);
setAccessKey("");
setSecretKey("");
};
const closeCredentialsModal = () => {
setNewServiceAccount(null);
navigate(`${IAM_PAGES.ACCOUNT}`);
};
return (
<Fragment>
{newServiceAccount !== null && (
<CredentialsPrompt
newServiceAccount={newServiceAccount}
open={true}
closeModal={() => {
closeCredentialsModal();
}}
entity="Access Key"
/>
)}
<Grid item xs={12}>
<PageHeaderWrapper
label={
<BackLink
label={"Access Keys"}
onClick={() => navigate(IAM_PAGES.ACCOUNT)}
/>
}
actions={<HelpMenu />}
/>
<PageLayout>
<FormLayout
helpBox={<AddServiceAccountHelpBox />}
icon={<ServiceAccountCredentialsIcon />}
title={"Create Access Key"}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
addServiceAccount(e);
}}
>
<InputBox
value={accessKey}
label={"Access Key"}
id={"accessKey"}
name={"accessKey"}
placeholder={"Enter Access Key"}
onChange={(e) => {
setAccessKey(e.target.value);
}}
startIcon={<ServiceAccountIcon />}
/>
<InputBox
value={secretKey}
label={"Secret Key"}
id={"secretKey"}
name={"secretKey"}
type={"password"}
placeholder={"Enter Secret Key"}
onChange={(e) => {
setSecretKey(e.target.value);
}}
startIcon={<PasswordKeyIcon />}
/>
<Switch
value="serviceAccountPolicy"
id="serviceAccountPolicy"
name="serviceAccountPolicy"
checked={isRestrictedByPolicy}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setIsRestrictedByPolicy(event.target.checked);
}}
label={"Restrict beyond user policy"}
description={
"You can specify an optional JSON-formatted IAM policy to further restrict Access Key access to a subset of the actions and resources explicitly allowed for the parent user. Additional access beyond that of the parent user cannot be implemented through these policies."
}
/>
{isRestrictedByPolicy && (
<Grid item xs={12}>
<Box>
<HelpTip
content={
<Fragment>
<a
target="blank"
href="https://min.io/docs/minio/kubernetes/upstream/administration/identity-access-management/policy-based-access-control.html#policy-document-structure"
>
Guide to access policy structure
</a>
</Fragment>
}
placement="right"
>
<PanelTitle>
Current User Policy - edit the JSON to remove
permissions for this Access Key
</PanelTitle>
</HelpTip>
</Box>
<Grid item xs={12} sx={{ ...modalStyleUtils.formScrollable }}>
<CodeMirrorWrapper
value={policyJSON}
onChange={(value) => {
setPolicyJSON(value);
}}
editorHeight={"350px"}
/>
</Grid>
</Grid>
)}
<Grid
xs={12}
sx={{
display: "flex",
alignItems: "center",
justifyContent: "start",
fontWeight: 600,
color: "rgb(7, 25, 62)",
gap: 2,
marginBottom: "15px",
marginTop: "15px",
}}
>
<Box
sx={{
marginTop: "15px",
width: "100%",
"& label": { width: "180px" },
}}
>
<DateTimeInput
noLabelMinWidth
value={expiry}
onChange={(e) => {
setExpiry(e);
}}
id="expiryTime"
label={"Expiry"}
timeFormat={"24h"}
secondsSelector={false}
/>
</Box>
</Grid>
<InputBox
value={name}
label={"Name"}
id={"name"}
name={"name"}
type={"text"}
placeholder={"Enter a name"}
onChange={(e) => {
setName(e.target.value);
}}
/>
<InputBox
value={description}
label={"Description"}
id={"description"}
name={"description"}
type={"text"}
placeholder={"Enter a description"}
onChange={(e) => {
setDescription(e.target.value);
}}
/>
<InputBox
value={comments}
label={"Comments"}
id={"comment"}
name={"comment"}
type={"text"}
placeholder={"Enter a comment"}
onChange={(e) => {
setComments(e.target.value);
}}
/>
<Grid item xs={12} sx={{ ...modalStyleUtils.modalButtonBar }}>
<Button
id={"clear"}
type="button"
variant="regular"
onClick={resetForm}
label={"Clear"}
/>
<Button
id={"create-sa"}
type="submit"
variant="callAction"
color="primary"
label={"Create"}
/>
</Grid>
</form>
</FormLayout>
</PageLayout>
</Grid>
</Fragment>
);
};
export default AddServiceAccount;

View File

@@ -1,212 +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, useState } from "react";
import {
Button,
ChangePasswordIcon,
InputBox,
Grid,
FormLayout,
ProgressBar,
InformativeMessage,
} from "mds";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import { modalStyleUtils } from "../Common/FormComponents/common/styleLibrary";
import {
setErrorSnackMessage,
setModalErrorSnackMessage,
setSnackBarMessage,
} from "../../../systemSlice";
import { useAppDispatch } from "../../../store";
import { api } from "api";
import { AccountChangePasswordRequest, ApiError } from "api/consoleApi";
import { errorToHandler } from "api/errors";
interface IChangePasswordProps {
open: boolean;
closeModal: () => void;
}
const ChangePassword = ({ open, closeModal }: IChangePasswordProps) => {
const dispatch = useAppDispatch();
const [currentPassword, setCurrentPassword] = useState<string>("");
const [newPassword, setNewPassword] = useState<string>("");
const [reNewPassword, setReNewPassword] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const userLoggedIn = localStorage.getItem("userLoggedIn") || "";
const changePassword = (event: React.FormEvent) => {
event.preventDefault();
if (newPassword !== reNewPassword) {
dispatch(
setModalErrorSnackMessage({
errorMessage: "New passwords don't match",
detailedError: "",
}),
);
return;
}
if (newPassword.length < 8) {
dispatch(
setModalErrorSnackMessage({
errorMessage: "Passwords must be at least 8 characters long",
detailedError: "",
}),
);
return;
}
if (loading) {
return;
}
setLoading(true);
let request: AccountChangePasswordRequest = {
current_secret_key: currentPassword,
new_secret_key: newPassword,
};
api.account
.accountChangePassword(request)
.then(() => {
setLoading(false);
setNewPassword("");
setReNewPassword("");
setCurrentPassword("");
dispatch(setSnackBarMessage("Successfully updated the password."));
closeModal();
})
.catch(async (res) => {
setLoading(false);
setNewPassword("");
setReNewPassword("");
setCurrentPassword("");
const err = (await res.json()) as ApiError;
dispatch(setErrorSnackMessage(errorToHandler(err)));
});
};
return open ? (
<ModalWrapper
title={`Change Password for ${userLoggedIn}`}
modalOpen={open}
onClose={() => {
setNewPassword("");
setReNewPassword("");
setCurrentPassword("");
closeModal();
}}
titleIcon={<ChangePasswordIcon />}
>
<div>
This will change your Console password. Please note your new password
down, as it will be required to log into Console after this session.
</div>
<InformativeMessage
variant={"warning"}
title={"Warning"}
message={
<Fragment>
If you are looking to change MINIO_ROOT_USER credentials, <br />
Please refer to{" "}
<a
target="_blank"
rel="noopener"
href="https://min.io/docs/minio/linux/administration/identity-access-management/minio-user-management.html#id4?ref=con"
>
rotating
</a>{" "}
credentials.
</Fragment>
}
sx={{ margin: "15px 0" }}
/>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
changePassword(e);
}}
>
<Grid container>
<Grid item xs={12} sx={{ ...modalStyleUtils.modalFormScrollable }}>
<FormLayout withBorders={false} containerPadding={false}>
<InputBox
id="current-password"
name="current-password"
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setCurrentPassword(event.target.value);
}}
label="Current Password"
type={"password"}
value={currentPassword}
/>
<InputBox
id="new-password"
name="new-password"
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setNewPassword(event.target.value);
}}
label="New Password"
type={"password"}
value={newPassword}
/>
<InputBox
id="re-new-password"
name="re-new-password"
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setReNewPassword(event.target.value);
}}
label="Type New Password Again"
type={"password"}
value={reNewPassword}
/>
</FormLayout>
</Grid>
<Grid item xs={12} sx={{ ...modalStyleUtils.modalButtonBar }}>
<Button
id={"save-password-modal"}
type="submit"
variant="callAction"
color="primary"
disabled={
loading ||
!(
currentPassword.length > 0 &&
newPassword.length > 0 &&
reNewPassword.length > 0
)
}
label="Save"
/>
</Grid>
{loading && (
<Grid item xs={12}>
<ProgressBar />
</Grid>
)}
</Grid>
</form>
</ModalWrapper>
) : null;
};
export default ChangePassword;

View File

@@ -1,166 +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 {
Box,
Button,
ChangePasswordIcon,
FormLayout,
InputBox,
ProgressBar,
} from "mds";
import { modalStyleUtils } from "../Common/FormComponents/common/styleLibrary";
import {
setErrorSnackMessage,
setModalErrorSnackMessage,
setSnackBarMessage,
} from "../../../systemSlice";
import { useAppDispatch } from "../../../store";
import { api } from "api";
import { ApiError, ChangeUserPasswordRequest } from "api/consoleApi";
import { errorToHandler } from "api/errors";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
interface IChangeUserPasswordProps {
open: boolean;
userName: string;
closeModal: () => void;
}
const ChangeUserPassword = ({
open,
userName,
closeModal,
}: IChangeUserPasswordProps) => {
const dispatch = useAppDispatch();
const [newPassword, setNewPassword] = useState<string>("");
const [reNewPassword, setReNewPassword] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const changeUserPassword = (event: React.FormEvent) => {
event.preventDefault();
if (loading) {
return;
}
setLoading(true);
if (newPassword.length < 8) {
dispatch(
setModalErrorSnackMessage({
errorMessage: "Passwords must be at least 8 characters long",
detailedError: "",
}),
);
setLoading(false);
return;
}
let request: ChangeUserPasswordRequest = {
selectedUser: userName,
newSecretKey: newPassword,
};
api.account
.changeUserPassword(request)
.then((res) => {
setLoading(false);
setNewPassword("");
setReNewPassword("");
dispatch(
setSnackBarMessage(
`Successfully updated the password for the user ${userName}.`,
),
);
closeModal();
})
.catch(async (res) => {
setLoading(false);
setNewPassword("");
setReNewPassword("");
const err = (await res.json()) as ApiError;
dispatch(setErrorSnackMessage(errorToHandler(err)));
});
};
return open ? (
<ModalWrapper
title="Change User Password"
modalOpen={open}
onClose={() => {
setNewPassword("");
setReNewPassword("");
closeModal();
}}
titleIcon={<ChangePasswordIcon />}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
changeUserPassword(e);
}}
>
<FormLayout withBorders={false} containerPadding={false}>
<Box sx={{ margin: "10px 0 20px" }}>
Change password for: <strong>{userName}</strong>
</Box>
<InputBox
id="new-password"
name="new-password"
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setNewPassword(event.target.value);
}}
label="New Password"
type="password"
value={newPassword}
/>
<InputBox
id="re-new-password"
name="re-new-password"
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setReNewPassword(event.target.value);
}}
label="Type New Password Again"
type="password"
value={reNewPassword}
/>
<Box sx={modalStyleUtils.modalButtonBar}>
<Button
id={"save-user-password"}
type="submit"
variant="callAction"
color="primary"
disabled={
loading ||
!(reNewPassword.length > 0 && newPassword === reNewPassword)
}
label={"Save"}
/>
</Box>
{loading && (
<Box>
<ProgressBar />
</Box>
)}
</FormLayout>
</form>
</ModalWrapper>
) : null;
};
export default ChangeUserPassword;

View File

@@ -1,89 +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, useState } from "react";
import ConfirmDialog from "../Common/ModalWrapper/ConfirmDialog";
import { ConfirmDeleteIcon } from "mds";
import { setErrorSnackMessage } from "../../../systemSlice";
import { useAppDispatch } from "../../../store";
import { api } from "api";
import { ApiError, HttpResponse } from "api/consoleApi";
import { errorToHandler } from "api/errors";
interface IDeleteServiceAccountProps {
closeDeleteModalAndRefresh: (refresh: boolean) => void;
deleteOpen: boolean;
selectedServiceAccount: string | null;
}
const DeleteServiceAccount = ({
closeDeleteModalAndRefresh,
deleteOpen,
selectedServiceAccount,
}: IDeleteServiceAccountProps) => {
const dispatch = useAppDispatch();
const onClose = () => closeDeleteModalAndRefresh(false);
const [loadingDelete, setLoadingDelete] = useState<boolean>(false);
if (!selectedServiceAccount) {
return null;
}
const onConfirmDelete = () => {
setLoadingDelete(true);
api.serviceAccounts
.deleteServiceAccount(selectedServiceAccount)
.then((_) => {
closeDeleteModalAndRefresh(true);
})
.catch(async (res: HttpResponse<void, ApiError>) => {
const err = (await res.json()) as ApiError;
dispatch(setErrorSnackMessage(errorToHandler(err)));
closeDeleteModalAndRefresh(false);
})
.finally(() => setLoadingDelete(false));
};
return (
<ConfirmDialog
title={`Delete Access Key`}
confirmText={"Delete"}
isOpen={deleteOpen}
titleIcon={<ConfirmDeleteIcon />}
isLoading={loadingDelete}
onConfirm={onConfirmDelete}
onClose={onClose}
confirmationContent={
<Fragment>
Are you sure you want to delete Access Key{" "}
<b
style={{
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word",
}}
>
{selectedServiceAccount}
</b>
?
</Fragment>
}
/>
);
};
export default DeleteServiceAccount;

View File

@@ -1,260 +0,0 @@
// 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, { useEffect, useState, Fragment } from "react";
import {
Box,
Button,
ChangeAccessPolicyIcon,
DateTimeInput,
Grid,
InputBox,
Switch,
} from "mds";
import { api } from "api";
import { errorToHandler } from "api/errors";
import CodeMirrorWrapper from "../Common/FormComponents/CodeMirrorWrapper/CodeMirrorWrapper";
import { ApiError } from "api/consoleApi";
import { useAppDispatch } from "store";
import { setErrorSnackMessage, setModalErrorSnackMessage } from "systemSlice";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import { modalStyleUtils } from "../Common/FormComponents/common/styleLibrary";
import { DateTime } from "luxon";
interface IServiceAccountPolicyProps {
open: boolean;
selectedAccessKey: string | null;
closeModalAndRefresh: () => void;
}
const EditServiceAccount = ({
open,
selectedAccessKey,
closeModalAndRefresh,
}: IServiceAccountPolicyProps) => {
const dispatch = useAppDispatch();
const [loading, setLoading] = useState<boolean>(false);
const [policyDefinition, setPolicyDefinition] = useState<any>("");
const [name, setName] = useState<string>("");
const [description, setDescription] = useState<string>("");
const [expiry, setExpiry] = useState<any>();
const [status, setStatus] = useState<string | undefined>("enabled");
useEffect(() => {
if (!loading && selectedAccessKey !== "") {
setLoading(true);
api.serviceAccounts
.getServiceAccount(selectedAccessKey || "")
.then((res) => {
setLoading(false);
const saInfo = res.data;
setName(saInfo?.name || "");
if (saInfo?.expiration) {
setExpiry(DateTime.fromISO(saInfo?.expiration));
}
setDescription(saInfo?.description || "");
setStatus(saInfo.accountStatus);
setPolicyDefinition(saInfo.policy || "");
})
.catch((err) => {
setLoading(false);
dispatch(setModalErrorSnackMessage(errorToHandler(err)));
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedAccessKey]);
const setPolicy = (event: React.FormEvent, newPolicy: string) => {
event.preventDefault();
api.serviceAccounts
.updateServiceAccount(selectedAccessKey || "", {
policy: newPolicy,
description: description,
expiry: expiry,
name: name,
status: status,
})
.then(() => {
closeModalAndRefresh();
})
.catch(async (res) => {
const err = (await res.json()) as ApiError;
dispatch(setErrorSnackMessage(errorToHandler(err)));
});
};
return (
<ModalWrapper
title={`Edit details of - ${selectedAccessKey}`}
modalOpen={open}
onClose={() => {
closeModalAndRefresh();
}}
titleIcon={<ChangeAccessPolicyIcon />}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
setPolicy(e, policyDefinition);
}}
>
<Grid container>
<Grid item xs={12}>
<CodeMirrorWrapper
label={`Access Key Policy`}
value={policyDefinition}
onChange={(value) => {
setPolicyDefinition(value);
}}
editorHeight={"350px"}
helptip={
<Fragment>
<a
target="blank"
href="https://min.io/docs/minio/kubernetes/upstream/administration/identity-access-management/policy-based-access-control.html#policy-document-structure"
>
Guide to access policy structure
</a>
</Fragment>
}
/>
</Grid>
<Box
sx={{
marginBottom: "15px",
marginTop: "15px",
display: "flex",
width: "100%",
"& label": { width: "195px" },
}}
>
<DateTimeInput
noLabelMinWidth
value={expiry}
onChange={(e) => {
setExpiry(e);
}}
id="expiryTime"
label={"Expiry"}
timeFormat={"24h"}
secondsSelector={false}
/>
</Box>
<Grid
xs={12}
sx={{
marginBottom: "15px",
}}
>
<InputBox
value={name}
size={120}
label={"Name"}
id={"name"}
name={"name"}
type={"text"}
placeholder={"Enter a name"}
onChange={(e) => {
setName(e.target.value);
}}
/>
</Grid>
<Grid
xs={12}
sx={{
marginBottom: "15px",
}}
>
<InputBox
size={120}
value={description}
label={"Description"}
id={"description"}
name={"description"}
type={"text"}
placeholder={"Enter a description"}
onChange={(e) => {
setDescription(e.target.value);
}}
/>
</Grid>
<Grid
xs={12}
sx={{
display: "flex",
alignItems: "center",
justifyContent: "start",
fontWeight: 600,
color: "rgb(7, 25, 62)",
gap: 2,
marginBottom: "15px",
}}
>
<label style={{ width: "150px" }}>Status</label>
<Box
sx={{
padding: "2px",
}}
>
<Switch
style={{
gap: "115px",
}}
indicatorLabels={["Enabled", "Disabled"]}
checked={status === "on"}
id="saStatus"
name="saStatus"
label=""
onChange={(e) => {
setStatus(e.target.checked ? "on" : "off");
}}
value="yes"
/>
</Box>
</Grid>
<Grid item xs={12} sx={modalStyleUtils.modalButtonBar}>
<Button
id={"cancel-sa-policy"}
type="button"
variant="regular"
onClick={() => {
closeModalAndRefresh();
}}
disabled={loading}
label={"Cancel"}
/>
<Button
id={"save-sa-policy"}
type="submit"
variant="callAction"
color="primary"
disabled={loading}
label={"Update"}
/>
</Grid>
</Grid>
</form>
</ModalWrapper>
);
};
export default EditServiceAccount;

View File

@@ -1,37 +0,0 @@
// 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 from "react";
import { HelpBox, LambdaNotificationsIcon, Box } from "mds";
const NotificationEndpointTypeSelectorHelpBox = () => {
return (
<HelpBox
iconComponent={<LambdaNotificationsIcon />}
title={"What are Event Destinations?"}
help={
<Box sx={{ paddingTop: "20px" }}>
MinIO bucket notifications allow administrators to send notifications
to supported external services on certain object or bucket events.
MinIO supports bucket and object-level S3 events similar to the Amazon
S3 Event Notifications.
</Box>
}
/>
);
};
export default NotificationEndpointTypeSelectorHelpBox;

View File

@@ -1,233 +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 { useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import { DataTable, SectionTitle, Tabs, HelpTip } from "mds";
import { api } from "api";
import { errorToHandler } from "api/errors";
import {
CONSOLE_UI_RESOURCE,
IAM_PAGES,
IAM_SCOPES,
} from "../../../../common/SecureComponent/permissions";
import {
hasPermission,
SecureComponent,
} from "../../../../common/SecureComponent";
import { setErrorSnackMessage, setHelpName } from "../../../../systemSlice";
import { selBucketDetailsLoading } from "./bucketDetailsSlice";
import { useAppDispatch } from "../../../../store";
import { Policy } from "../../../../api/consoleApi";
const AccessDetails = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const params = useParams();
const loadingBucket = useSelector(selBucketDetailsLoading);
const [curTab, setCurTab] = useState<string>("simple-tab-0");
const [loadingPolicies, setLoadingPolicies] = useState<boolean>(true);
const [bucketPolicy, setBucketPolicy] = useState<Policy[] | undefined>([]);
const [loadingUsers, setLoadingUsers] = useState<boolean>(true);
const [bucketUsers, setBucketUsers] = useState<string[]>([]);
const bucketName = params.bucketName || "";
const displayPoliciesList = hasPermission(bucketName, [
IAM_SCOPES.ADMIN_LIST_USER_POLICIES,
]);
const displayUsersList = hasPermission(
bucketName,
[
IAM_SCOPES.ADMIN_GET_POLICY,
IAM_SCOPES.ADMIN_LIST_USERS,
IAM_SCOPES.ADMIN_LIST_GROUPS,
],
true,
);
const viewUser = hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.ADMIN_GET_USER,
]);
const viewPolicy = hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.ADMIN_GET_POLICY,
IAM_SCOPES.ADMIN_LIST_USERS,
IAM_SCOPES.ADMIN_LIST_GROUPS,
]);
useEffect(() => {
if (loadingBucket) {
setLoadingUsers(true);
setLoadingPolicies(true);
}
}, [loadingBucket, setLoadingUsers, setLoadingPolicies]);
const PolicyActions = [
{
type: "view",
disableButtonFunction: () => !viewPolicy,
onClick: (policy: any) => {
navigate(`${IAM_PAGES.POLICIES}/${encodeURIComponent(policy.name)}`);
},
},
];
const userTableActions = [
{
type: "view",
disableButtonFunction: () => !viewUser,
onClick: (user: any) => {
navigate(`${IAM_PAGES.USERS}/${encodeURIComponent(user)}`);
},
},
];
useEffect(() => {
if (loadingUsers) {
if (displayUsersList) {
api.bucketUsers
.listUsersWithAccessToBucket(bucketName)
.then((res) => {
setBucketUsers(res.data);
setLoadingUsers(false);
})
.catch((err) => {
dispatch(setErrorSnackMessage(errorToHandler(err)));
setLoadingUsers(false);
});
} else {
setLoadingUsers(false);
}
}
}, [loadingUsers, dispatch, bucketName, displayUsersList]);
useEffect(() => {
dispatch(setHelpName("bucket_detail_access"));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (loadingPolicies) {
if (displayPoliciesList) {
api.bucketPolicy
.listPoliciesWithBucket(bucketName)
.then((res) => {
setBucketPolicy(res.data.policies);
setLoadingPolicies(false);
})
.catch((err) => {
dispatch(setErrorSnackMessage(errorToHandler(err)));
setLoadingPolicies(false);
});
} else {
setLoadingPolicies(false);
}
}
}, [loadingPolicies, dispatch, bucketName, displayPoliciesList]);
return (
<Fragment>
<SectionTitle separator>
<HelpTip
content={
<Fragment>
Understand which{" "}
<a
target="blank"
href="https://min.io/docs/minio/linux/administration/identity-access-management/policy-based-access-control.html#"
>
Policies
</a>{" "}
and{" "}
<a
target="blank"
href="https://min.io/docs/minio/linux/administration/identity-access-management/minio-user-management.html"
>
Users
</a>{" "}
are authorized to access this Bucket.
</Fragment>
}
placement="right"
>
Access Audit
</HelpTip>
</SectionTitle>
<Tabs
currentTabOrPath={curTab}
onTabClick={(newValue: string) => {
setCurTab(newValue);
}}
horizontal
options={[
{
tabConfig: { label: "Policies", id: "simple-tab-0" },
content: (
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_LIST_USER_POLICIES]}
resource={bucketName}
errorProps={{ disabled: true }}
>
{bucketPolicy && (
<DataTable
noBackground={true}
itemActions={PolicyActions}
columns={[{ label: "Name", elementKey: "name" }]}
isLoading={loadingPolicies}
records={bucketPolicy}
entityName="Policies"
idField="name"
/>
)}
</SecureComponent>
),
},
{
tabConfig: { label: "Users", id: "simple-tab-1" },
content: (
<SecureComponent
scopes={[
IAM_SCOPES.ADMIN_GET_POLICY,
IAM_SCOPES.ADMIN_LIST_USERS,
IAM_SCOPES.ADMIN_LIST_GROUPS,
]}
resource={bucketName}
matchAll
errorProps={{ disabled: true }}
>
<DataTable
noBackground={true}
itemActions={userTableActions}
columns={[{ label: "User", elementKey: "accessKey" }]}
isLoading={loadingUsers}
records={bucketUsers}
entityName="Users"
idField="accessKey"
/>
</SecureComponent>
),
},
]}
/>
</Fragment>
);
};
export default AccessDetails;

View File

@@ -1,244 +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 { AddIcon, Button, DataTable, SectionTitle, HelpTip } from "mds";
import { useSelector } from "react-redux";
import { useParams } from "react-router-dom";
import { api } from "api";
import { AccessRule as IAccessRule } from "api/consoleApi";
import { errorToHandler } from "api/errors";
import { IAM_SCOPES } from "../../../../common/SecureComponent/permissions";
import {
hasPermission,
SecureComponent,
} from "../../../../common/SecureComponent";
import { setErrorSnackMessage, setHelpName } from "../../../../systemSlice";
import { selBucketDetailsLoading } from "./bucketDetailsSlice";
import { useAppDispatch } from "../../../../store";
import withSuspense from "../../Common/Components/withSuspense";
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
const AddAccessRuleModal = withSuspense(
React.lazy(() => import("./AddAccessRule")),
);
const DeleteAccessRuleModal = withSuspense(
React.lazy(() => import("./DeleteAccessRule")),
);
const EditAccessRuleModal = withSuspense(
React.lazy(() => import("./EditAccessRule")),
);
const AccessRule = () => {
const dispatch = useAppDispatch();
const params = useParams();
const loadingBucket = useSelector(selBucketDetailsLoading);
const [loadingAccessRules, setLoadingAccessRules] = useState<boolean>(true);
const [accessRules, setAccessRules] = useState<IAccessRule[] | undefined>([]);
const [addAccessRuleOpen, setAddAccessRuleOpen] = useState<boolean>(false);
const [deleteAccessRuleOpen, setDeleteAccessRuleOpen] =
useState<boolean>(false);
const [accessRuleToDelete, setAccessRuleToDelete] = useState<string>("");
const [editAccessRuleOpen, setEditAccessRuleOpen] = useState<boolean>(false);
const [accessRuleToEdit, setAccessRuleToEdit] = useState<string>("");
const [initialAccess, setInitialAccess] = useState<string>("");
const bucketName = params.bucketName || "";
const displayAccessRules = hasPermission(bucketName, [
IAM_SCOPES.S3_GET_BUCKET_POLICY,
IAM_SCOPES.S3_GET_ACTIONS,
]);
const deleteAccessRules = hasPermission(bucketName, [
IAM_SCOPES.S3_DELETE_BUCKET_POLICY,
]);
const editAccessRules = hasPermission(bucketName, [
IAM_SCOPES.S3_PUT_BUCKET_POLICY,
IAM_SCOPES.S3_PUT_ACTIONS,
]);
useEffect(() => {
if (loadingBucket) {
setLoadingAccessRules(true);
}
}, [loadingBucket, setLoadingAccessRules]);
const AccessRuleActions = [
{
type: "delete",
disableButtonFunction: () => !deleteAccessRules,
onClick: (accessRule: any) => {
setDeleteAccessRuleOpen(true);
setAccessRuleToDelete(accessRule.prefix);
},
},
{
type: "view",
disableButtonFunction: () => !editAccessRules,
onClick: (accessRule: any) => {
setAccessRuleToEdit(accessRule.prefix);
setInitialAccess(accessRule.access);
setEditAccessRuleOpen(true);
},
},
];
useEffect(() => {
dispatch(setHelpName("bucket_detail_prefix"));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (loadingAccessRules) {
if (displayAccessRules) {
api.bucket
.listAccessRulesWithBucket(bucketName)
.then((res) => {
setAccessRules(res.data.accessRules);
setLoadingAccessRules(false);
})
.catch((err) => {
dispatch(setErrorSnackMessage(errorToHandler(err)));
setLoadingAccessRules(false);
});
} else {
setLoadingAccessRules(false);
}
}
}, [loadingAccessRules, dispatch, displayAccessRules, bucketName]);
const closeAddAccessRuleModal = () => {
setAddAccessRuleOpen(false);
setLoadingAccessRules(true);
};
const closeDeleteAccessRuleModal = () => {
setDeleteAccessRuleOpen(false);
setLoadingAccessRules(true);
};
const closeEditAccessRuleModal = () => {
setEditAccessRuleOpen(false);
setLoadingAccessRules(true);
};
return (
<Fragment>
{addAccessRuleOpen && (
<AddAccessRuleModal
modalOpen={addAccessRuleOpen}
onClose={closeAddAccessRuleModal}
bucket={bucketName}
/>
)}
{deleteAccessRuleOpen && (
<DeleteAccessRuleModal
modalOpen={deleteAccessRuleOpen}
onClose={closeDeleteAccessRuleModal}
bucket={bucketName}
toDelete={accessRuleToDelete}
/>
)}
{editAccessRuleOpen && (
<EditAccessRuleModal
modalOpen={editAccessRuleOpen}
onClose={closeEditAccessRuleModal}
bucket={bucketName}
toEdit={accessRuleToEdit}
initial={initialAccess}
/>
)}
<SectionTitle
separator
sx={{ marginBottom: 15 }}
actions={
<SecureComponent
scopes={[
IAM_SCOPES.S3_GET_BUCKET_POLICY,
IAM_SCOPES.S3_PUT_BUCKET_POLICY,
IAM_SCOPES.S3_GET_ACTIONS,
IAM_SCOPES.S3_PUT_ACTIONS,
]}
resource={bucketName}
matchAll
errorProps={{ disabled: true }}
>
<TooltipWrapper tooltip={"Add Access Rule"}>
<Button
id={"add-bucket-access-rule"}
onClick={() => {
setAddAccessRuleOpen(true);
}}
label={"Add Access Rule"}
icon={<AddIcon />}
variant={"callAction"}
/>
</TooltipWrapper>
</SecureComponent>
}
>
<HelpTip
content={
<Fragment>
Setting an{" "}
<a
href="https://min.io/docs/minio/linux/reference/minio-mc/mc-anonymous-set.html"
target="blank"
>
Anonymous
</a>{" "}
policy allows clients to access the Bucket or prefix contents and
perform actions consistent with the specified policy without
authentication.
</Fragment>
}
placement="right"
>
Anonymous Access
</HelpTip>
</SectionTitle>
<SecureComponent
scopes={[IAM_SCOPES.S3_GET_BUCKET_POLICY, IAM_SCOPES.S3_GET_ACTIONS]}
resource={bucketName}
errorProps={{ disabled: true }}
>
<DataTable
itemActions={AccessRuleActions}
columns={[
{
label: "Prefix",
elementKey: "prefix",
renderFunction: (prefix: string) => {
return prefix || "/";
},
},
{ label: "Access", elementKey: "access" },
]}
isLoading={loadingAccessRules}
records={accessRules || []}
entityName="Access Rules"
idField="prefix"
/>
</SecureComponent>
</Fragment>
);
};
export default AccessRule;

View File

@@ -1,149 +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, { useEffect, useState, Fragment } from "react";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import {
AddAccessRuleIcon,
Button,
FormLayout,
Grid,
InputBox,
Select,
} from "mds";
import { api } from "api";
import { errorToHandler } from "api/errors";
import { modalStyleUtils } from "../../Common/FormComponents/common/styleLibrary";
import {
setErrorSnackMessage,
setSnackBarMessage,
} from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
interface IAddAccessRule {
modalOpen: boolean;
onClose: () => any;
bucket: string;
prefilledRoute?: string;
}
const AddAccessRule = ({
modalOpen,
onClose,
bucket,
prefilledRoute,
}: IAddAccessRule) => {
const dispatch = useAppDispatch();
const [prefix, setPrefix] = useState("");
const [selectedAccess, setSelectedAccess] = useState<any>("readonly");
useEffect(() => {
if (prefilledRoute) {
setPrefix(prefilledRoute);
}
}, [prefilledRoute]);
const accessOptions = [
{ label: "readonly", value: "readonly" },
{ label: "writeonly", value: "writeonly" },
{ label: "readwrite", value: "readwrite" },
];
const resetForm = () => {
setPrefix("");
setSelectedAccess("readonly");
};
const createProcess = () => {
api.bucket
.setAccessRuleWithBucket(bucket, {
prefix: prefix,
access: selectedAccess,
})
.then((res: any) => {
dispatch(setSnackBarMessage("Access Rule added successfully"));
onClose();
})
.catch((res) => {
dispatch(setErrorSnackMessage(errorToHandler(res.error)));
onClose();
});
};
return (
<ModalWrapper
modalOpen={modalOpen}
title="Add Anonymous Access Rule"
onClose={onClose}
titleIcon={<AddAccessRuleIcon />}
>
<FormLayout withBorders={false} containerPadding={false}>
<InputBox
value={prefix}
label={"Prefix"}
id={"prefix"}
name={"prefix"}
placeholder={"Enter Prefix"}
onChange={(e) => {
setPrefix(e.target.value);
}}
tooltip={
"Enter '/' to apply the rule to all prefixes and objects at the bucket root. Do not include the wildcard asterisk '*' as part of the prefix *unless* it is an explicit part of the prefix name. The Console automatically appends an asterisk to the appropriate sections of the resulting IAM policy."
}
/>
<Select
id="access"
name="Access"
onChange={(value) => {
setSelectedAccess(value);
}}
label="Access"
value={selectedAccess}
options={accessOptions}
disabled={false}
helpTip={
<Fragment>
Select the desired level of access available to unauthenticated
Users
</Fragment>
}
helpTipPlacement="right"
/>
<Grid item xs={12} sx={modalStyleUtils.modalButtonBar}>
<Button
id={"clear"}
type="button"
variant="regular"
onClick={resetForm}
label={"Clear"}
/>
<Button
id={"add-access-save"}
type="submit"
variant="callAction"
disabled={prefix.trim() === ""}
onClick={createProcess}
label={"Save"}
/>
</Grid>
</FormLayout>
</ModalWrapper>
);
};
export default AddAccessRule;

View File

@@ -1,472 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2023 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import {
BackLink,
Box,
BucketReplicationIcon,
Button,
FormLayout,
Grid,
HelpBox,
InputBox,
PageLayout,
Select,
Switch,
} from "mds";
import { IAM_PAGES } from "../../../../common/SecureComponent/permissions";
import { setErrorSnackMessage, setHelpName } from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import PageHeaderWrapper from "../../Common/PageHeaderWrapper/PageHeaderWrapper";
import HelpMenu from "../../HelpMenu";
import { api } from "api";
import { errorToHandler } from "api/errors";
import QueryMultiSelector from "screens/Console/Common/FormComponents/QueryMultiSelector/QueryMultiSelector";
import { getBytes, k8sScalarUnitsExcluding } from "common/utils";
import get from "lodash/get";
import InputUnitMenu from "screens/Console/Common/FormComponents/InputUnitMenu/InputUnitMenu";
const AddBucketReplication = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
let params = new URLSearchParams(document.location.search);
const bucketName = params.get("bucketName") || "";
const nextPriority = params.get("nextPriority") || "1";
const [addLoading, setAddLoading] = useState<boolean>(false);
const [priority, setPriority] = useState<string>(nextPriority);
const [accessKey, setAccessKey] = useState<string>("");
const [secretKey, setSecretKey] = useState<string>("");
const [targetURL, setTargetURL] = useState<string>("");
const [targetStorageClass, setTargetStorageClass] = useState<string>("");
const [prefix, setPrefix] = useState<string>("");
const [targetBucket, setTargetBucket] = useState<string>("");
const [region, setRegion] = useState<string>("");
const [useTLS, setUseTLS] = useState<boolean>(true);
const [repDeleteMarker, setRepDeleteMarker] = useState<boolean>(true);
const [repDelete, setRepDelete] = useState<boolean>(true);
const [metadataSync, setMetadataSync] = useState<boolean>(true);
const [repExisting, setRepExisting] = useState<boolean>(true);
const [tags, setTags] = useState<string>("");
const [replicationMode, setReplicationMode] = useState<"async" | "sync">(
"async",
);
const [bandwidthScalar, setBandwidthScalar] = useState<string>("100");
const [bandwidthUnit, setBandwidthUnit] = useState<string>("Gi");
const [healthCheck, setHealthCheck] = useState<string>("60");
const [validated, setValidated] = useState<boolean>(false);
const backLink = IAM_PAGES.BUCKETS + `/${bucketName}/admin/replication`;
useEffect(() => {
dispatch(setHelpName("bucket-replication-add"));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const addRecord = () => {
const replicate = [
{
originBucket: bucketName,
destinationBucket: targetBucket,
},
];
const hc = parseInt(healthCheck);
const endURL = `${useTLS ? "https://" : "http://"}${targetURL}`;
const remoteBucketsInfo = {
accessKey: accessKey,
secretKey: secretKey,
targetURL: endURL,
region: region,
bucketsRelation: replicate,
syncMode: replicationMode,
bandwidth:
replicationMode === "async"
? parseInt(getBytes(bandwidthScalar, bandwidthUnit, true))
: 0,
healthCheckPeriod: hc,
prefix: prefix,
tags: tags,
replicateDeleteMarkers: repDeleteMarker,
replicateDeletes: repDelete,
replicateExistingObjects: repExisting,
priority: parseInt(priority),
storageClass: targetStorageClass,
replicateMetadata: metadataSync,
};
api.bucketsReplication
.setMultiBucketReplication(remoteBucketsInfo)
.then((res) => {
setAddLoading(false);
const states = get(res.data, "replicationState", []);
if (states.length > 0) {
const itemVal = states[0];
setAddLoading(false);
if (itemVal.errorString && itemVal.errorString !== "") {
dispatch(
setErrorSnackMessage({
errorMessage: "There was an error",
detailedError: itemVal.errorString,
}),
);
// navigate(backLink);
return;
}
navigate(backLink);
return;
}
dispatch(
setErrorSnackMessage({
errorMessage: "No changes applied",
detailedError: "",
}),
);
})
.catch((err) => {
console.log("this is an error!");
setAddLoading(false);
dispatch(setErrorSnackMessage(errorToHandler(err.error)));
});
};
useEffect(() => {
!validated &&
accessKey.length >= 3 &&
secretKey.length >= 8 &&
targetBucket.length >= 3 &&
targetURL.length > 0 &&
setValidated(true);
}, [targetURL, accessKey, secretKey, targetBucket, validated]);
useEffect(() => {
if (
validated &&
(accessKey.length < 3 ||
secretKey.length < 8 ||
targetBucket.length < 3 ||
targetURL.length < 1)
) {
setValidated(false);
}
}, [targetURL, accessKey, secretKey, targetBucket, validated]);
return (
<Fragment>
<PageHeaderWrapper
label={
<BackLink
label={"Add Bucket Replication Rule - " + bucketName}
onClick={() => navigate(backLink)}
/>
}
actions={<HelpMenu />}
/>
<PageLayout>
<FormLayout
title="Add Replication Rule"
icon={<BucketReplicationIcon />}
helpBox={
<HelpBox
iconComponent={<BucketReplicationIcon />}
title="Bucket Replication Configuration"
help={
<Fragment>
<Box sx={{ paddconngTop: "10px" }}>
The bucket selected in this deployment acts as the source
while the configured remote deployment acts as the target.
</Box>
<Box sx={{ paddingTop: "10px" }}>
For each write operation to this "source" bucket, MinIO
checks all configured replication rules and applies the
matching rule with highest configured priority.
</Box>
<Box sx={{ paddingTop: "10px" }}>
MinIO supports automatically replicating existing objects in
a bucket; this setting is enabled by default. Please note
that objects created before replication was configured or
while replication is disabled are not synchronized to the
target deployment in case this setting is not enabled.
</Box>
<Box sx={{ paddingTop: "10px" }}>
MinIO supports replicating delete operations, where MinIO
synchronizes deleting specific object versions and new
delete markers. Delete operation replication uses the same
replication process as all other replication operations.
</Box>{" "}
</Fragment>
}
/>
}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setAddLoading(true);
addRecord();
}}
>
<InputBox
id="priority"
name="priority"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.validity.valid) {
setPriority(e.target.value);
}
}}
label="Priority"
value={priority}
pattern={"[0-9]*"}
/>
<InputBox
id="targetURL"
name="targetURL"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTargetURL(e.target.value);
}}
placeholder="play.min.io"
label="Target URL"
value={targetURL}
/>
<Switch
checked={useTLS}
id="useTLS"
name="useTLS"
label="Use TLS"
onChange={(e) => {
setUseTLS(e.target.checked);
}}
value="yes"
/>
<InputBox
id="accessKey"
name="accessKey"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setAccessKey(e.target.value);
}}
label="Access Key"
value={accessKey}
/>
<InputBox
id="secretKey"
name="secretKey"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setSecretKey(e.target.value);
}}
label="Secret Key"
value={secretKey}
/>
<InputBox
id="targetBucket"
name="targetBucket"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTargetBucket(e.target.value);
}}
label="Target Bucket"
value={targetBucket}
/>
<InputBox
id="region"
name="region"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setRegion(e.target.value);
}}
label="Region"
value={region}
/>
<Select
id="replication_mode"
name="replication_mode"
onChange={(value) => {
setReplicationMode(value as "async" | "sync");
}}
label="Replication Mode"
value={replicationMode}
options={[
{ label: "Asynchronous", value: "async" },
{ label: "Synchronous", value: "sync" },
]}
/>
{replicationMode === "async" && (
<Box className={"inputItem"}>
<InputBox
type="number"
id="bandwidth_scalar"
name="bandwidth_scalar"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.validity.valid) {
setBandwidthScalar(e.target.value as string);
}
}}
label="Bandwidth"
value={bandwidthScalar}
min="0"
pattern={"[0-9]*"}
overlayObject={
<InputUnitMenu
id={"quota_unit"}
onUnitChange={(newValue) => {
setBandwidthUnit(newValue);
}}
unitSelected={bandwidthUnit}
unitsList={k8sScalarUnitsExcluding(["Ki"])}
disabled={false}
/>
}
/>
</Box>
)}
<InputBox
id="healthCheck"
name="healthCheck"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setHealthCheck(e.target.value as string);
}}
label="Health Check Duration"
value={healthCheck}
/>
<InputBox
id="storageClass"
name="storageClass"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTargetStorageClass(e.target.value);
}}
placeholder="STANDARD_IA,REDUCED_REDUNDANCY etc"
label="Storage Class"
value={targetStorageClass}
/>
<fieldset className={"inputItem"}>
<legend>Object Filters</legend>
<InputBox
id="prefix"
name="prefix"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setPrefix(e.target.value);
}}
placeholder="prefix"
label="Prefix"
value={prefix}
/>
<QueryMultiSelector
name="tags"
label="Tags"
elements={""}
onChange={(vl: string) => {
setTags(vl);
}}
keyPlaceholder="Tag Key"
valuePlaceholder="Tag Value"
withBorder
/>
</fieldset>
<fieldset className={"inputItem"}>
<legend>Replication Options</legend>
<Switch
checked={repExisting}
id="repExisting"
name="repExisting"
label="Existing Objects"
onChange={(e) => {
setRepExisting(e.target.checked);
}}
description={"Replicate existing objects"}
/>
<Switch
checked={metadataSync}
id="metadatataSync"
name="metadatataSync"
label="Metadata Sync"
onChange={(e) => {
setMetadataSync(e.target.checked);
}}
description={"Metadata Sync"}
/>
<Switch
checked={repDeleteMarker}
id="deleteMarker"
name="deleteMarker"
label="Delete Marker"
onChange={(e) => {
setRepDeleteMarker(e.target.checked);
}}
description={"Replicate soft deletes"}
/>
<Switch
checked={repDelete}
id="repDelete"
name="repDelete"
label="Deletes"
onChange={(e) => {
setRepDelete(e.target.checked);
}}
description={"Replicate versioned deletes"}
/>
</fieldset>
<Grid
item
xs={12}
sx={{
display: "flex",
flexDirection: "row",
justifyContent: "end",
gap: 10,
paddingTop: 10,
}}
>
<Button
id={"cancel"}
type="button"
variant="regular"
disabled={addLoading}
onClick={() => {
navigate(backLink);
}}
label={"Cancel"}
/>
<Button
id={"submit"}
type="submit"
variant="callAction"
color="primary"
disabled={addLoading || !validated}
label={"Save"}
/>
</Grid>
</form>
</FormLayout>
</PageLayout>
</Fragment>
);
};
export default AddBucketReplication;

View File

@@ -1,128 +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 { AddNewTagIcon, Box, Button, FormLayout, Grid, InputBox } from "mds";
import { modalStyleUtils } from "../../Common/FormComponents/common/styleLibrary";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import { setModalErrorSnackMessage } from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import { api } from "api";
import { errorToHandler } from "api/errors";
interface IBucketTagModal {
modalOpen: boolean;
currentTags: any;
bucketName: string;
onCloseAndUpdate: (refresh: boolean) => void;
}
const AddBucketTagModal = ({
modalOpen,
currentTags,
onCloseAndUpdate,
bucketName,
}: IBucketTagModal) => {
const dispatch = useAppDispatch();
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 };
api.buckets
.putBucketTags(bucketName, {
tags: newTagList,
})
.then(() => {
setIsSending(false);
onCloseAndUpdate(true);
})
.catch((error) => {
dispatch(setModalErrorSnackMessage(errorToHandler(error.error)));
setIsSending(false);
});
};
return (
<ModalWrapper
modalOpen={modalOpen}
title={`Add New Tag `}
onClose={() => {
onCloseAndUpdate(false);
}}
titleIcon={<AddNewTagIcon />}
>
<FormLayout withBorders={false} containerPadding={false}>
<Box sx={{ marginBottom: 15 }}>
<strong>Bucket</strong>: {bucketName}
</Box>
<InputBox
value={newKey}
label={"New Tag Key"}
id={"newTagKey"}
name={"newTagKey"}
placeholder={"Enter New Tag Key"}
onChange={(e: any) => {
setNewKey(e.target.value);
}}
/>
<InputBox
value={newLabel}
label={"New Tag Label"}
id={"newTagLabel"}
name={"newTagLabel"}
placeholder={"Enter New Tag Label"}
onChange={(e: any) => {
setNewLabel(e.target.value);
}}
/>
<Grid item xs={12} sx={modalStyleUtils.modalButtonBar}>
<Button
id={"clear"}
type="button"
variant="regular"
onClick={resetForm}
label={"Clear"}
/>
<Button
id={"save-add-bucket-tag"}
type="submit"
variant="callAction"
color="primary"
disabled={
newLabel.trim() === "" || newKey.trim() === "" || isSending
}
onClick={addTagProcess}
label={"Save"}
/>
</Grid>
</FormLayout>
</ModalWrapper>
);
};
export default AddBucketTagModal;

View File

@@ -1,250 +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, { useCallback, useEffect, useState, Fragment } from "react";
import {
Autocomplete,
Button,
DataTable,
EventSubscriptionIcon,
Grid,
InputBox,
} from "mds";
import { ErrorResponseHandler } from "../../../../common/types";
import { setModalErrorSnackMessage } from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import { api } from "api";
import { NotificationEventType } from "api/consoleApi";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import {
formFieldStyles,
modalBasic,
modalStyleUtils,
} from "../../Common/FormComponents/common/styleLibrary";
interface IAddEventProps {
open: boolean;
selectedBucket: string;
closeModalAndRefresh: () => void;
}
const AddEvent = ({
open,
selectedBucket,
closeModalAndRefresh,
}: IAddEventProps) => {
const dispatch = useAppDispatch();
const [addLoading, setAddLoading] = useState<boolean>(false);
const [prefix, setPrefix] = useState<string>("");
const [suffix, setSuffix] = useState<string>("");
const [arn, setArn] = useState<string>("");
const [selectedEvents, setSelectedEvents] = useState<NotificationEventType[]>(
[],
);
const [arnList, setArnList] = useState<string[] | undefined>([]);
const addRecord = (event: React.FormEvent) => {
event.preventDefault();
if (addLoading) {
return;
}
setAddLoading(true);
api.buckets
.createBucketEvent(selectedBucket, {
configuration: {
arn: arn,
events: selectedEvents,
prefix: prefix,
suffix: suffix,
},
ignoreExisting: true,
})
.then(() => {
setAddLoading(false);
closeModalAndRefresh();
})
.catch((err: ErrorResponseHandler) => {
setAddLoading(false);
dispatch(setModalErrorSnackMessage(err));
});
};
const fetchArnList = useCallback(() => {
setAddLoading(true);
api.admin
.arnList()
.then((res) => {
if (res.data.arns !== null) {
setArnList(res.data.arns);
}
setAddLoading(false);
})
.catch((err: ErrorResponseHandler) => {
setAddLoading(false);
dispatch(setModalErrorSnackMessage(err));
});
}, [dispatch]);
useEffect(() => {
fetchArnList();
}, [fetchArnList]);
const events = [
{ label: "PUT - Object Uploaded", value: NotificationEventType.Put },
{ label: "GET - Object accessed", value: NotificationEventType.Get },
{ label: "DELETE - Object Deleted", value: NotificationEventType.Delete },
{
label: "REPLICA - Object Replicated",
value: NotificationEventType.Replica,
},
{ label: "ILM - Object Transitioned", value: NotificationEventType.Ilm },
{
label:
"SCANNER - Object has too many versions / Prefixes has too many sub-folders",
value: NotificationEventType.Scanner,
},
];
const handleClick = (event: React.ChangeEvent<HTMLInputElement>) => {
const targetD = event.target;
const value = targetD.value;
const checked = targetD.checked;
let elements: NotificationEventType[] = [...selectedEvents];
if (checked) {
elements.push(value as NotificationEventType);
} else {
elements = elements.filter((element) => element !== value);
}
setSelectedEvents(elements);
};
const arnValues = arnList?.map((arnConstant) => ({
label: arnConstant,
value: arnConstant,
}));
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
closeModalAndRefresh();
}}
title="Subscribe To Bucket Events"
titleIcon={<EventSubscriptionIcon />}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
addRecord(e);
}}
>
<Grid container>
<Grid item xs={12} sx={modalBasic.formScrollable}>
<Grid
item
xs={12}
sx={{
...formFieldStyles.formFieldRow,
"& div div .MuiOutlinedInput-root": {
padding: 0,
},
}}
>
<Autocomplete
onChange={(value: string) => {
setArn(value);
}}
id="select-access-policy"
name="select-access-policy"
label={"ARN"}
value={arn}
options={arnValues || []}
helpTip={
<Fragment>
<a
target="blank"
href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html"
>
Amazon Resource Name
</a>
</Fragment>
}
/>
</Grid>
<Grid item xs={12} sx={formFieldStyles.formFieldRow}>
<InputBox
id="prefix-input"
name="prefix-input"
label="Prefix"
value={prefix}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setPrefix(e.target.value);
}}
/>
</Grid>
<Grid item xs={12} sx={formFieldStyles.formFieldRow}>
<InputBox
id="suffix-input"
name="suffix-input"
label="Suffix"
value={suffix}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setSuffix(e.target.value);
}}
/>
</Grid>
<Grid item xs={12} sx={formFieldStyles.formFieldRow}>
<DataTable
columns={[{ label: "Event", elementKey: "label" }]}
idField={"value"}
records={events}
onSelect={handleClick}
selectedItems={selectedEvents}
noBackground
customPaperHeight={"260px"}
/>
</Grid>
</Grid>
<Grid item xs={12} sx={modalStyleUtils.modalButtonBar}>
<Button
id={"cancel-add-event"}
type="button"
variant="regular"
disabled={addLoading}
onClick={() => {
closeModalAndRefresh();
}}
label={"Cancel"}
/>
<Button
id={"save-event"}
type="submit"
variant="callAction"
disabled={addLoading || arn === "" || selectedEvents.length === 0}
label={"Save"}
/>
</Grid>
</Grid>
</form>
</ModalWrapper>
);
};
export default AddEvent;

View File

@@ -1,101 +0,0 @@
// 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, { Fragment, useState } from "react";
import { Grid, InputBox } from "mds";
import { useAppDispatch } from "../../../../store";
import { setErrorSnackMessage } from "../../../../systemSlice";
import ConfirmDialog from "../../Common/ModalWrapper/ConfirmDialog";
import KMSHelpBox from "../../KMS/KMSHelpbox";
import { api } from "api";
import { ApiError, HttpResponse } from "api/consoleApi";
import { errorToHandler } from "api/errors";
interface IAddKeyModalProps {
closeAddModalAndRefresh: (refresh: boolean) => void;
addOpen: boolean;
}
const AddKeyModal = ({
closeAddModalAndRefresh,
addOpen,
}: IAddKeyModalProps) => {
const dispatch = useAppDispatch();
const onClose = () => closeAddModalAndRefresh(false);
const [loadingAdd, setLoadingAdd] = useState<boolean>(false);
const [keyName, setKeyName] = useState<string>("");
const onConfirmAdd = () => {
setLoadingAdd(true);
api.kms
.kmsCreateKey({ key: keyName })
.then((_) => {
closeAddModalAndRefresh(true);
})
.catch(async (res: HttpResponse<void, ApiError>) => {
const err = (await res.json()) as ApiError;
dispatch(setErrorSnackMessage(errorToHandler(err)));
closeAddModalAndRefresh(false);
})
.finally(() => setLoadingAdd(false));
};
return (
<ConfirmDialog
title={""}
confirmText={"Create"}
isOpen={addOpen}
isLoading={loadingAdd}
onConfirm={onConfirmAdd}
onClose={onClose}
confirmButtonProps={{
disabled: keyName.indexOf(" ") !== -1 || keyName === "" || loadingAdd,
variant: "callAction",
}}
confirmationContent={
<Fragment>
<KMSHelpBox
helpText={"Create Key"}
contents={[
"Create a new cryptographic key in the Key Management Service server connected to MINIO.",
]}
/>
<Grid item xs={12} sx={{ marginTop: 15 }}>
<InputBox
id="key-name"
name="key-name"
label="Key Name"
autoFocus={true}
value={keyName}
error={
keyName.indexOf(" ") !== -1
? "Key name cannot contain spaces"
: ""
}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setKeyName(e.target.value);
}}
/>
</Grid>
</Fragment>
}
/>
);
};
export default AddKeyModal;

View File

@@ -1,407 +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, { useEffect, useState } from "react";
import get from "lodash/get";
import {
Box,
BucketReplicationIcon,
Button,
FormLayout,
Grid,
InputBox,
Select,
Switch,
} from "mds";
import { api } from "api";
import { errorToHandler } from "api/errors";
import { modalStyleUtils } from "../../Common/FormComponents/common/styleLibrary";
import { BucketReplicationRule } from "../types";
import { getBytes, k8sScalarUnitsExcluding } from "../../../../common/utils";
import { setModalErrorSnackMessage } from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import QueryMultiSelector from "../../Common/FormComponents/QueryMultiSelector/QueryMultiSelector";
import InputUnitMenu from "../../Common/FormComponents/InputUnitMenu/InputUnitMenu";
interface IReplicationModal {
open: boolean;
closeModalAndRefresh: () => any;
bucketName: string;
setReplicationRules: BucketReplicationRule[];
}
const AddReplicationModal = ({
open,
closeModalAndRefresh,
bucketName,
setReplicationRules,
}: IReplicationModal) => {
const dispatch = useAppDispatch();
const [addLoading, setAddLoading] = useState<boolean>(false);
const [priority, setPriority] = useState<string>("1");
const [accessKey, setAccessKey] = useState<string>("");
const [secretKey, setSecretKey] = useState<string>("");
const [targetURL, setTargetURL] = useState<string>("");
const [targetStorageClass, setTargetStorageClass] = useState<string>("");
const [prefix, setPrefix] = useState<string>("");
const [targetBucket, setTargetBucket] = useState<string>("");
const [region, setRegion] = useState<string>("");
const [useTLS, setUseTLS] = useState<boolean>(true);
const [repDeleteMarker, setRepDeleteMarker] = useState<boolean>(true);
const [repDelete, setRepDelete] = useState<boolean>(true);
const [metadataSync, setMetadataSync] = useState<boolean>(true);
const [tags, setTags] = useState<string>("");
const [replicationMode, setReplicationMode] = useState<"async" | "sync">(
"async",
);
const [bandwidthScalar, setBandwidthScalar] = useState<string>("100");
const [bandwidthUnit, setBandwidthUnit] = useState<string>("Gi");
const [healthCheck, setHealthCheck] = useState<string>("60");
useEffect(() => {
if (setReplicationRules.length === 0) {
setPriority("1");
return;
}
const greatestValue = setReplicationRules.reduce((prevAcc, currValue) => {
if (currValue.priority > prevAcc) {
return currValue.priority;
}
return prevAcc;
}, 0);
const nextPriority = greatestValue + 1;
setPriority(nextPriority.toString());
}, [setReplicationRules]);
const addRecord = () => {
const replicate = [
{
originBucket: bucketName,
destinationBucket: targetBucket,
},
];
const hc = parseInt(healthCheck);
const endURL = `${useTLS ? "https://" : "http://"}${targetURL}`;
const remoteBucketsInfo = {
accessKey: accessKey,
secretKey: secretKey,
targetURL: endURL,
region: region,
bucketsRelation: replicate,
syncMode: replicationMode,
bandwidth:
replicationMode === "async"
? parseInt(getBytes(bandwidthScalar, bandwidthUnit, true))
: 0,
healthCheckPeriod: hc,
prefix: prefix,
tags: tags,
replicateDeleteMarkers: repDeleteMarker,
replicateDeletes: repDelete,
priority: parseInt(priority),
storageClass: targetStorageClass,
replicateMetadata: metadataSync,
};
api.bucketsReplication
.setMultiBucketReplication(remoteBucketsInfo)
.then((res) => {
setAddLoading(false);
const states = get(res.data, "replicationState", []);
if (states.length > 0) {
const itemVal = states[0];
setAddLoading(false);
if (itemVal.errorString && itemVal.errorString !== "") {
dispatch(
setModalErrorSnackMessage({
errorMessage: itemVal.errorString,
detailedError: "",
}),
);
return;
}
closeModalAndRefresh();
return;
}
dispatch(
setModalErrorSnackMessage({
errorMessage: "No changes applied",
detailedError: "",
}),
);
})
.catch((err) => {
setAddLoading(false);
dispatch(setModalErrorSnackMessage(errorToHandler(err.error)));
});
};
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
closeModalAndRefresh();
}}
title="Set Bucket Replication"
titleIcon={<BucketReplicationIcon />}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setAddLoading(true);
addRecord();
}}
>
<FormLayout withBorders={false} containerPadding={false}>
<InputBox
id="priority"
name="priority"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.validity.valid) {
setPriority(e.target.value);
}
}}
label="Priority"
value={priority}
pattern={"[0-9]*"}
/>
<InputBox
id="targetURL"
name="targetURL"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTargetURL(e.target.value);
}}
placeholder="play.min.io"
label="Target URL"
value={targetURL}
/>
<Switch
checked={useTLS}
id="useTLS"
name="useTLS"
label="Use TLS"
onChange={(e) => {
setUseTLS(e.target.checked);
}}
value="yes"
/>
<InputBox
id="accessKey"
name="accessKey"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setAccessKey(e.target.value);
}}
label="Access Key"
value={accessKey}
/>
<InputBox
id="secretKey"
name="secretKey"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setSecretKey(e.target.value);
}}
label="Secret Key"
value={secretKey}
/>
<InputBox
id="targetBucket"
name="targetBucket"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTargetBucket(e.target.value);
}}
label="Target Bucket"
value={targetBucket}
/>
<InputBox
id="region"
name="region"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setRegion(e.target.value);
}}
label="Region"
value={region}
/>
<Select
id="replication_mode"
name="replication_mode"
onChange={(value) => {
setReplicationMode(value as "async" | "sync");
}}
label="Replication Mode"
value={replicationMode}
options={[
{ label: "Asynchronous", value: "async" },
{ label: "Synchronous", value: "sync" },
]}
/>
{replicationMode === "async" && (
<Box className={"inputItem"}>
<InputBox
type="number"
id="bandwidth_scalar"
name="bandwidth_scalar"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.validity.valid) {
setBandwidthScalar(e.target.value as string);
}
}}
label="Bandwidth"
value={bandwidthScalar}
min="0"
pattern={"[0-9]*"}
overlayObject={
<InputUnitMenu
id={"quota_unit"}
onUnitChange={(newValue) => {
setBandwidthUnit(newValue);
}}
unitSelected={bandwidthUnit}
unitsList={k8sScalarUnitsExcluding(["Ki"])}
disabled={false}
/>
}
/>
</Box>
)}
<InputBox
id="healthCheck"
name="healthCheck"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setHealthCheck(e.target.value as string);
}}
label="Health Check Duration"
value={healthCheck}
/>
<InputBox
id="storageClass"
name="storageClass"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTargetStorageClass(e.target.value);
}}
placeholder="STANDARD_IA,REDUCED_REDUNDANCY etc"
label="Storage Class"
value={targetStorageClass}
/>
<fieldset className={"inputItem"}>
<legend>Object Filters</legend>
<InputBox
id="prefix"
name="prefix"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setPrefix(e.target.value);
}}
placeholder="prefix"
label="Prefix"
value={prefix}
/>
<QueryMultiSelector
name="tags"
label="Tags"
elements={""}
onChange={(vl: string) => {
setTags(vl);
}}
keyPlaceholder="Tag Key"
valuePlaceholder="Tag Value"
withBorder
/>
</fieldset>
<fieldset className={"inputItem"}>
<legend>Replication Options</legend>
<Switch
checked={metadataSync}
id="metadatataSync"
name="metadatataSync"
label="Metadata Sync"
onChange={(e) => {
setMetadataSync(e.target.checked);
}}
description={"Metadata Sync"}
/>
<Switch
checked={repDeleteMarker}
id="deleteMarker"
name="deleteMarker"
label="Delete Marker"
onChange={(e) => {
setRepDeleteMarker(e.target.checked);
}}
description={"Replicate soft deletes"}
/>
<Switch
checked={repDelete}
id="repDelete"
name="repDelete"
label="Deletes"
onChange={(e) => {
setRepDelete(e.target.checked);
}}
description={"Replicate versioned deletes"}
/>
</fieldset>
<Grid item xs={12} sx={modalStyleUtils.modalButtonBar}>
<Button
id={"cancel"}
type="button"
variant="regular"
disabled={addLoading}
onClick={() => {
closeModalAndRefresh();
}}
label={"Cancel"}
/>
<Button
id={"submit"}
type="submit"
variant="callAction"
color="primary"
disabled={addLoading}
label={"Save"}
/>
</Grid>
</FormLayout>
</form>
</ModalWrapper>
);
};
export default AddReplicationModal;

View File

@@ -27,7 +27,6 @@ import {
setLoadingObjectInfo,
setLoadingVersioning,
setLoadingVersions,
setLockingEnabled,
setObjectDetailsView,
setRequestInProgress,
setSelectedObjectView,
@@ -58,9 +57,6 @@ const BrowserHandler = () => {
const requestInProgress = useSelector(
(state: AppState) => state.objectBrowser.requestInProgress,
);
const loadingLocking = useSelector(
(state: AppState) => state.objectBrowser.loadingLocking,
);
const reloadObjectsList = useSelector(
(state: AppState) => state.objectBrowser.reloadObjectsList,
);
@@ -209,29 +205,6 @@ const BrowserHandler = () => {
anonymousMode,
]);
useEffect(() => {
if (loadingLocking) {
if (displayListObjects) {
api.buckets
.getBucketObjectLockingStatus(bucketName)
.then((res) => {
dispatch(setLockingEnabled(res.data.object_locking_enabled));
dispatch(setLoadingLocking(false));
})
.catch((err) => {
console.error(
"Error Getting Object Locking Status: ",
err.error.detailedMessage,
);
dispatch(setLoadingLocking(false));
});
} else {
dispatch(resetMessages());
dispatch(setLoadingLocking(false));
}
}
}, [bucketName, loadingLocking, dispatch, displayListObjects]);
return (
<Fragment>
{!anonymousMode && <OBHeader bucketName={bucketName} />}

View File

@@ -1,380 +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 {
Navigate,
Route,
Routes,
useLocation,
useNavigate,
useParams,
} from "react-router-dom";
import {
BackLink,
Box,
BucketsIcon,
Button,
FolderIcon,
PageLayout,
RefreshIcon,
ScreenTitle,
Tabs,
TrashIcon,
} from "mds";
import { useSelector } from "react-redux";
import {
browseBucketPermissions,
deleteBucketPermissions,
IAM_PERMISSIONS,
IAM_ROLES,
IAM_SCOPES,
permissionTooltipHelper,
} from "../../../../common/SecureComponent/permissions";
import {
hasPermission,
SecureComponent,
} from "../../../../common/SecureComponent";
import withSuspense from "../../Common/Components/withSuspense";
import {
selDistSet,
selSiteRep,
setErrorSnackMessage,
setHelpName,
} from "../../../../systemSlice";
import {
selBucketDetailsInfo,
selBucketDetailsLoading,
setBucketDetailsLoad,
setBucketInfo,
} from "./bucketDetailsSlice";
import { useAppDispatch } from "../../../../store";
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
import PageHeaderWrapper from "../../Common/PageHeaderWrapper/PageHeaderWrapper";
import { api } from "api";
import { errorToHandler } from "api/errors";
import HelpMenu from "../../HelpMenu";
const DeleteBucket = withSuspense(
React.lazy(() => import("../ListBuckets/DeleteBucket")),
);
const AccessRulePanel = withSuspense(
React.lazy(() => import("./AccessRulePanel")),
);
const AccessDetailsPanel = withSuspense(
React.lazy(() => import("./AccessDetailsPanel")),
);
const BucketSummaryPanel = withSuspense(
React.lazy(() => import("./BucketSummaryPanel")),
);
const BucketEventsPanel = withSuspense(
React.lazy(() => import("./BucketEventsPanel")),
);
const BucketReplicationPanel = withSuspense(
React.lazy(() => import("./BucketReplicationPanel")),
);
const BucketDetails = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const params = useParams();
const location = useLocation();
const distributedSetup = useSelector(selDistSet);
const loadingBucket = useSelector(selBucketDetailsLoading);
const bucketInfo = useSelector(selBucketDetailsInfo);
const siteReplicationInfo = useSelector(selSiteRep);
const [iniLoad, setIniLoad] = useState<boolean>(false);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const bucketName = params.bucketName || "";
const canDelete = hasPermission(bucketName, deleteBucketPermissions);
const canBrowse = hasPermission(bucketName, browseBucketPermissions);
useEffect(() => {
dispatch(setHelpName("bucket_details"));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (!iniLoad) {
dispatch(setBucketDetailsLoad(true));
setIniLoad(true);
}
}, [iniLoad, dispatch, setIniLoad]);
useEffect(() => {
if (loadingBucket) {
api.buckets
.bucketInfo(bucketName)
.then((res) => {
dispatch(setBucketDetailsLoad(false));
dispatch(setBucketInfo(res.data));
})
.catch((err) => {
dispatch(setBucketDetailsLoad(false));
dispatch(setErrorSnackMessage(errorToHandler(err)));
});
}
}, [bucketName, loadingBucket, dispatch]);
let topLevelRoute = `/buckets/${bucketName}`;
const defaultRoute = "/admin/summary";
const manageBucketRoutes: Record<string, any> = {
events: "/admin/events",
replication: "/admin/replication",
access: "/admin/access",
prefix: "/admin/prefix",
};
const getRoutePath = (routeKey: string) => {
let path = manageBucketRoutes[routeKey];
if (!path) {
path = `${topLevelRoute}${defaultRoute}`;
} else {
path = `${topLevelRoute}${path}`;
}
return path;
};
const closeDeleteModalAndRefresh = (refresh: boolean) => {
setDeleteOpen(false);
if (refresh) {
navigate("/buckets");
}
};
return (
<Fragment>
{deleteOpen && (
<DeleteBucket
deleteOpen={deleteOpen}
selectedBucket={bucketName}
closeDeleteModalAndRefresh={(refresh: boolean) => {
closeDeleteModalAndRefresh(refresh);
}}
/>
)}
<PageHeaderWrapper
label={
<BackLink label={"Buckets"} onClick={() => navigate("/buckets")} />
}
actions={
<Fragment>
<TooltipWrapper
tooltip={
canBrowse
? "Browse Bucket"
: permissionTooltipHelper(
IAM_PERMISSIONS[IAM_ROLES.BUCKET_VIEWER],
"browsing this bucket",
)
}
>
<Button
id={"switch-browse-view"}
aria-label="Browse Bucket"
onClick={() => {
navigate(`/browser/${bucketName}`);
}}
icon={
<FolderIcon
style={{ width: 20, height: 20, marginTop: -3 }}
/>
}
style={{
padding: "0 10px",
}}
disabled={!canBrowse}
/>
</TooltipWrapper>
<HelpMenu />
</Fragment>
}
/>
<PageLayout>
<ScreenTitle
icon={
<Fragment>
<BucketsIcon width={40} />
</Fragment>
}
title={bucketName}
subTitle={
<SecureComponent
scopes={[
IAM_SCOPES.S3_GET_BUCKET_POLICY,
IAM_SCOPES.S3_GET_ACTIONS,
]}
resource={bucketName}
>
<span style={{ fontSize: 15 }}>Access: </span>
<span
style={{
fontWeight: 600,
fontSize: 15,
textTransform: "capitalize",
}}
>
{bucketInfo?.access?.toLowerCase()}
</span>
</SecureComponent>
}
actions={
<Fragment>
<SecureComponent
scopes={deleteBucketPermissions}
resource={bucketName}
errorProps={{ disabled: true }}
>
<TooltipWrapper
tooltip={
canDelete
? ""
: permissionTooltipHelper(
[
IAM_SCOPES.S3_DELETE_BUCKET,
IAM_SCOPES.S3_FORCE_DELETE_BUCKET,
],
"deleting this bucket",
)
}
>
<Button
id={"delete-bucket-button"}
onClick={() => {
setDeleteOpen(true);
}}
label={"Delete Bucket"}
icon={<TrashIcon />}
variant={"secondary"}
disabled={!canDelete}
/>
</TooltipWrapper>
</SecureComponent>
<Button
id={"refresh-bucket-info"}
onClick={() => {
dispatch(setBucketDetailsLoad(true));
}}
label={"Refresh"}
icon={<RefreshIcon />}
/>
</Fragment>
}
sx={{ marginBottom: 15 }}
/>
<Box>
<Tabs
currentTabOrPath={location.pathname}
useRouteTabs
onTabClick={(tab) => {
navigate(tab);
}}
options={[
{
tabConfig: {
label: "Summary",
id: "summary",
to: getRoutePath("summary"),
},
},
{
tabConfig: {
label: "Events",
id: "events",
disabled: !hasPermission(bucketName, [
IAM_SCOPES.S3_GET_BUCKET_NOTIFICATIONS,
IAM_SCOPES.S3_PUT_BUCKET_NOTIFICATIONS,
IAM_SCOPES.S3_GET_ACTIONS,
IAM_SCOPES.S3_PUT_ACTIONS,
]),
to: getRoutePath("events"),
},
},
{
tabConfig: {
label: "Replication",
id: "replication",
disabled:
!distributedSetup ||
(siteReplicationInfo.enabled &&
siteReplicationInfo.curSite) ||
!hasPermission(bucketName, [
IAM_SCOPES.S3_GET_REPLICATION_CONFIGURATION,
IAM_SCOPES.S3_PUT_REPLICATION_CONFIGURATION,
IAM_SCOPES.S3_GET_ACTIONS,
IAM_SCOPES.S3_PUT_ACTIONS,
]),
to: getRoutePath("replication"),
},
},
{
tabConfig: {
label: "Access",
id: "access",
disabled: !hasPermission(bucketName, [
IAM_SCOPES.ADMIN_GET_POLICY,
IAM_SCOPES.ADMIN_LIST_USER_POLICIES,
IAM_SCOPES.ADMIN_LIST_USERS,
]),
to: getRoutePath("access"),
},
},
{
tabConfig: {
label: "Anonymous",
id: "anonymous",
disabled: !hasPermission(bucketName, [
IAM_SCOPES.S3_GET_BUCKET_POLICY,
IAM_SCOPES.S3_GET_ACTIONS,
]),
to: getRoutePath("prefix"),
},
},
]}
routes={
<Routes>
<Route path="summary" element={<BucketSummaryPanel />} />
<Route path="events" element={<BucketEventsPanel />} />
{distributedSetup && (
<Route
path="replication"
element={<BucketReplicationPanel />}
/>
)}
<Route path="access" element={<AccessDetailsPanel />} />
<Route path="prefix" element={<AccessRulePanel />} />
<Route
path="*"
element={
<Navigate to={`/buckets/${bucketName}/admin/summary`} />
}
/>
</Routes>
}
/>
</Box>
</PageLayout>
</Fragment>
);
};
export default BucketDetails;

View File

@@ -1,263 +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 { useSelector } from "react-redux";
import { useParams } from "react-router-dom";
import {
AddIcon,
Button,
HelpBox,
LambdaIcon,
DataTable,
Grid,
SectionTitle,
HelpTip,
} from "mds";
import { api } from "api";
import { NotificationConfig } from "api/consoleApi";
import { errorToHandler } from "api/errors";
import {
hasPermission,
SecureComponent,
} from "../../../../common/SecureComponent";
import { IAM_SCOPES } from "../../../../common/SecureComponent/permissions";
import { setErrorSnackMessage, setHelpName } from "../../../../systemSlice";
import { selBucketDetailsLoading } from "./bucketDetailsSlice";
import { useAppDispatch } from "../../../../store";
import withSuspense from "../../Common/Components/withSuspense";
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
const DeleteEvent = withSuspense(React.lazy(() => import("./DeleteEvent")));
const AddEvent = withSuspense(React.lazy(() => import("./AddEvent")));
const BucketEventsPanel = () => {
const dispatch = useAppDispatch();
const params = useParams();
const loadingBucket = useSelector(selBucketDetailsLoading);
const [addEventScreenOpen, setAddEventScreenOpen] = useState<boolean>(false);
const [loadingEvents, setLoadingEvents] = useState<boolean>(true);
const [records, setRecords] = useState<NotificationConfig[]>([]);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [selectedEvent, setSelectedEvent] = useState<NotificationConfig | null>(
null,
);
const bucketName = params.bucketName || "";
const displayEvents = hasPermission(bucketName, [
IAM_SCOPES.S3_GET_BUCKET_NOTIFICATIONS,
IAM_SCOPES.S3_GET_ACTIONS,
]);
useEffect(() => {
if (loadingBucket) {
setLoadingEvents(true);
}
}, [loadingBucket, setLoadingEvents]);
useEffect(() => {
dispatch(setHelpName("bucket_detail_events"));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (loadingEvents) {
if (displayEvents) {
api.buckets
.listBucketEvents(bucketName)
.then((res) => {
const events = get(res.data, "events", []);
setLoadingEvents(false);
setRecords(events || []);
})
.catch((err) => {
setLoadingEvents(false);
dispatch(setErrorSnackMessage(errorToHandler(err.error)));
});
} else {
setLoadingEvents(false);
}
}
}, [loadingEvents, dispatch, bucketName, displayEvents]);
const eventsDisplay = (events: string[] | null) => {
if (!events) {
return "other";
}
const cleanEvents = events.reduce((acc: string[], read: string) => {
if (!acc.includes(read)) {
return [...acc, read];
}
return acc;
}, []);
return <Fragment>{cleanEvents.join(", ")}</Fragment>;
};
const confirmDeleteEvent = (evnt: NotificationConfig) => {
setDeleteOpen(true);
setSelectedEvent(evnt);
};
const closeAddEventAndRefresh = () => {
setAddEventScreenOpen(false);
setLoadingEvents(true);
};
const closeDeleteModalAndRefresh = (refresh: boolean) => {
setDeleteOpen(false);
if (refresh) {
setLoadingEvents(true);
}
};
const tableActions = [{ type: "delete", onClick: confirmDeleteEvent }];
return (
<Fragment>
{deleteOpen && (
<DeleteEvent
deleteOpen={deleteOpen}
selectedBucket={bucketName}
bucketEvent={selectedEvent}
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
)}
{addEventScreenOpen && (
<AddEvent
open={addEventScreenOpen}
selectedBucket={bucketName}
closeModalAndRefresh={closeAddEventAndRefresh}
/>
)}
<SectionTitle
separator
sx={{ marginBottom: 15 }}
actions={
<SecureComponent
scopes={[
IAM_SCOPES.S3_PUT_BUCKET_NOTIFICATIONS,
IAM_SCOPES.S3_PUT_ACTIONS,
IAM_SCOPES.ADMIN_SERVER_INFO,
]}
resource={bucketName}
matchAll
errorProps={{ disabled: true }}
>
<TooltipWrapper tooltip={"Subscribe to Event"}>
<Button
id={"Subscribe-bucket-event"}
onClick={() => {
setAddEventScreenOpen(true);
}}
label={"Subscribe to Event"}
icon={<AddIcon />}
variant={"callAction"}
/>
</TooltipWrapper>
</SecureComponent>
}
>
<HelpTip
content={
<Fragment>
MinIO{" "}
<a
target="blank"
href="https://min.io/docs/minio/kubernetes/upstream/administration/monitoring.html"
>
bucket notifications
</a>{" "}
allow administrators to send notifications to supported external
services on certain object or bucket events.
</Fragment>
}
placement="right"
>
Events
</HelpTip>
</SectionTitle>
<Grid container>
<Grid item xs={12}>
<SecureComponent
scopes={[
IAM_SCOPES.S3_GET_BUCKET_NOTIFICATIONS,
IAM_SCOPES.S3_GET_ACTIONS,
]}
resource={bucketName}
errorProps={{ disabled: true }}
>
<DataTable
itemActions={tableActions}
columns={[
{ label: "SQS", elementKey: "arn" },
{
label: "Events",
elementKey: "events",
renderFunction: eventsDisplay,
},
{ label: "Prefix", elementKey: "prefix" },
{ label: "Suffix", elementKey: "suffix" },
]}
isLoading={loadingEvents}
records={records}
entityName="Events"
idField="id"
customPaperHeight={"400px"}
/>
</SecureComponent>
</Grid>
{!loadingEvents && (
<Grid item xs={12}>
<br />
<HelpBox
title={"Event Notifications"}
iconComponent={<LambdaIcon />}
help={
<Fragment>
MinIO bucket notifications allow administrators to send
notifications to supported external services on certain object
or bucket events. MinIO supports bucket and object-level S3
events similar to the Amazon S3 Event Notifications.
<br />
<br />
You can learn more at our{" "}
<a
href="https://min.io/docs/minio/linux/administration/monitoring/bucket-notifications.html?ref=con"
target="_blank"
rel="noopener"
>
documentation
</a>
.
</Fragment>
}
/>
</Grid>
)}
</Grid>
</Fragment>
);
};
export default BucketEventsPanel;

View File

@@ -1,411 +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 { useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import {
AddIcon,
Box,
BucketsIcon,
Button,
DataTable,
Grid,
HelpBox,
SectionTitle,
TrashIcon,
HelpTip,
} from "mds";
import api from "../../../../common/api";
import {
BucketReplication,
BucketReplicationDestination,
BucketReplicationRule,
} from "../types";
import { ErrorResponseHandler } from "../../../../common/types";
import {
hasPermission,
SecureComponent,
} from "../../../../common/SecureComponent";
import {
IAM_PAGES,
IAM_SCOPES,
} from "../../../../common/SecureComponent/permissions";
import { setErrorSnackMessage, setHelpName } from "../../../../systemSlice";
import { selBucketDetailsLoading } from "./bucketDetailsSlice";
import { useAppDispatch } from "../../../../store";
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
import withSuspense from "../../Common/Components/withSuspense";
const EditReplicationModal = withSuspense(
React.lazy(() => import("./EditReplicationModal")),
);
const AddReplicationModal = withSuspense(
React.lazy(() => import("./AddReplicationModal")),
);
const DeleteReplicationRule = withSuspense(
React.lazy(() => import("./DeleteReplicationRule")),
);
const BucketReplicationPanel = () => {
const dispatch = useAppDispatch();
const params = useParams();
const loadingBucket = useSelector(selBucketDetailsLoading);
const [loadingReplication, setLoadingReplication] = useState<boolean>(true);
const [replicationRules, setReplicationRules] = useState<
BucketReplicationRule[]
>([]);
const [deleteReplicationModal, setDeleteReplicationModal] =
useState<boolean>(false);
const [openSetReplication, setOpenSetReplication] = useState<boolean>(false);
const [editReplicationModal, setEditReplicationModal] =
useState<boolean>(false);
const [selectedRRule, setSelectedRRule] = useState<string>("");
const [selectedRepRules, setSelectedRepRules] = useState<string[]>([]);
const [deleteSelectedRules, setDeleteSelectedRules] =
useState<boolean>(false);
const bucketName = params.bucketName || "";
const displayReplicationRules = hasPermission(bucketName, [
IAM_SCOPES.S3_GET_REPLICATION_CONFIGURATION,
IAM_SCOPES.S3_GET_ACTIONS,
]);
useEffect(() => {
dispatch(setHelpName("bucket_detail_replication"));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (loadingBucket) {
setLoadingReplication(true);
}
}, [loadingBucket, setLoadingReplication]);
useEffect(() => {
if (loadingReplication) {
if (displayReplicationRules) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/replication`)
.then((res: BucketReplication) => {
const r = res.rules ? res.rules : [];
r.sort((a, b) => a.priority - b.priority);
setReplicationRules(r);
setLoadingReplication(false);
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
setLoadingReplication(false);
});
} else {
setLoadingReplication(false);
}
}
}, [loadingReplication, dispatch, bucketName, displayReplicationRules]);
const closeAddReplication = () => {
setOpenReplicationOpen(false);
setLoadingReplication(true);
};
const setOpenReplicationOpen = (open = false) => {
setOpenSetReplication(open);
};
const closeReplicationModalDelete = (refresh: boolean) => {
setDeleteReplicationModal(false);
if (refresh) {
setLoadingReplication(true);
}
};
const closeEditReplication = (refresh: boolean) => {
setEditReplicationModal(false);
if (refresh) {
setLoadingReplication(true);
}
};
const confirmDeleteReplication = (replication: BucketReplicationRule) => {
setSelectedRRule(replication.id);
setDeleteSelectedRules(false);
setDeleteReplicationModal(true);
};
const confirmDeleteSelectedReplicationRules = () => {
setSelectedRRule("selectedRules");
setDeleteSelectedRules(true);
setDeleteReplicationModal(true);
};
const navigate = useNavigate();
const editReplicationRule = (replication: BucketReplicationRule) => {
setSelectedRRule(replication.id);
navigate(
`/buckets/edit-replication?bucketName=${bucketName}&ruleID=${replication.id}`,
);
};
const ruleDestDisplay = (events: BucketReplicationDestination) => {
return <Fragment>{events.bucket.replace("arn:aws:s3:::", "")}</Fragment>;
};
const tagDisplay = (events: BucketReplicationRule) => {
return <Fragment>{events && events.tags !== "" ? "Yes" : "No"}</Fragment>;
};
const selectAllItems = () => {
if (selectedRepRules.length === replicationRules.length) {
setSelectedRepRules([]);
return;
}
setSelectedRepRules(replicationRules.map((x) => x.id));
};
const selectRules = (e: React.ChangeEvent<HTMLInputElement>) => {
const targetD = e.target;
const value = targetD.value;
const checked = targetD.checked;
let elements: string[] = [...selectedRepRules]; // We clone the selectedSAs array
if (checked) {
// If the user has checked this field we need to push this to selectedSAs
elements.push(value);
} else {
// User has unchecked this field, we need to remove it from the list
elements = elements.filter((element) => element !== value);
}
setSelectedRepRules(elements);
return elements;
};
const replicationTableActions: any = [
{
type: "delete",
onClick: confirmDeleteReplication,
},
{
type: "view",
onClick: editReplicationRule,
disableButtonFunction: !hasPermission(
bucketName,
[
IAM_SCOPES.S3_PUT_REPLICATION_CONFIGURATION,
IAM_SCOPES.S3_PUT_ACTIONS,
],
true,
),
},
];
return (
<Fragment>
{openSetReplication && (
<AddReplicationModal
closeModalAndRefresh={closeAddReplication}
open={openSetReplication}
bucketName={bucketName}
setReplicationRules={replicationRules}
/>
)}
{deleteReplicationModal && (
<DeleteReplicationRule
deleteOpen={deleteReplicationModal}
selectedBucket={bucketName}
closeDeleteModalAndRefresh={closeReplicationModalDelete}
ruleToDelete={selectedRRule}
rulesToDelete={selectedRepRules}
remainingRules={replicationRules.length}
allSelected={
replicationRules.length > 0 &&
selectedRepRules.length === replicationRules.length
}
deleteSelectedRules={deleteSelectedRules}
/>
)}
{editReplicationModal && (
<EditReplicationModal
closeModalAndRefresh={closeEditReplication}
open={editReplicationModal}
bucketName={bucketName}
ruleID={selectedRRule}
/>
)}
<SectionTitle
separator
sx={{ marginBottom: 15 }}
actions={
<Box style={{ display: "flex", gap: 10 }}>
<SecureComponent
scopes={[
IAM_SCOPES.S3_PUT_REPLICATION_CONFIGURATION,
IAM_SCOPES.S3_PUT_ACTIONS,
]}
resource={bucketName}
matchAll
errorProps={{ disabled: true }}
>
<TooltipWrapper tooltip={"Remove Selected Replication Rules"}>
<Button
id={"remove-bucket-replication-rule"}
onClick={() => {
confirmDeleteSelectedReplicationRules();
}}
label={"Remove Selected Rules"}
icon={<TrashIcon />}
color={"secondary"}
disabled={
selectedRepRules.length === 0 ||
replicationRules.length === 0
}
variant={"secondary"}
/>
</TooltipWrapper>
</SecureComponent>
<SecureComponent
scopes={[
IAM_SCOPES.S3_PUT_REPLICATION_CONFIGURATION,
IAM_SCOPES.S3_PUT_ACTIONS,
]}
resource={bucketName}
matchAll
errorProps={{ disabled: true }}
>
<TooltipWrapper tooltip={"Add Replication Rule"}>
<Button
id={"add-bucket-replication-rule"}
onClick={() => {
navigate(
IAM_PAGES.BUCKETS_ADD_REPLICATION +
`?bucketName=${bucketName}&nextPriority=${
replicationRules.length + 1
}`,
);
}}
label={"Add Replication Rule"}
icon={<AddIcon />}
variant={"callAction"}
/>
</TooltipWrapper>
</SecureComponent>
</Box>
}
>
<HelpTip
content={
<Fragment>
MinIO{" "}
<a
target="blank"
href="https://min.io/docs/minio/kubernetes/upstream/administration/bucket-replication.html"
>
server-side bucket replication
</a>{" "}
is an automatic bucket-level configuration that synchronizes
objects between a source and destination bucket.
</Fragment>
}
placement="right"
>
Replication
</HelpTip>
</SectionTitle>
<Grid container>
<Grid item xs={12}>
<SecureComponent
scopes={[
IAM_SCOPES.S3_GET_REPLICATION_CONFIGURATION,
IAM_SCOPES.S3_GET_ACTIONS,
]}
resource={bucketName}
errorProps={{ disabled: true }}
>
<DataTable
itemActions={replicationTableActions}
columns={[
{
label: "Priority",
elementKey: "priority",
width: 55,
contentTextAlign: "center",
},
{
label: "Destination",
elementKey: "destination",
renderFunction: ruleDestDisplay,
},
{
label: "Prefix",
elementKey: "prefix",
width: 200,
},
{
label: "Tags",
elementKey: "tags",
renderFunction: tagDisplay,
width: 60,
},
{ label: "Status", elementKey: "status", width: 100 },
]}
isLoading={loadingReplication}
records={replicationRules}
entityName="Replication Rules"
idField="id"
customPaperHeight={"400px"}
textSelectable
selectedItems={selectedRepRules}
onSelect={(e) => selectRules(e)}
onSelectAll={selectAllItems}
/>
</SecureComponent>
</Grid>
<Grid item xs={12}>
<br />
<HelpBox
title={"Replication"}
iconComponent={<BucketsIcon />}
help={
<Fragment>
MinIO supports server-side and client-side replication of
objects between source and destination buckets.
<br />
<br />
You can learn more at our{" "}
<a
href="https://min.io/docs/minio/linux/administration/bucket-replication.html?ref=con"
target="_blank"
rel="noopener"
>
documentation
</a>
.
</Fragment>
}
/>
</Grid>
</Grid>
</Fragment>
);
};
export default BucketReplicationPanel;

View File

@@ -1,727 +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 { useSelector } from "react-redux";
import { useParams } from "react-router-dom";
import { api } from "api";
import {
BucketEncryptionInfo,
BucketQuota,
BucketVersioningResponse,
GetBucketRetentionConfig,
} from "api/consoleApi";
import { errorToHandler } from "api/errors";
import {
Box,
DisabledIcon,
EnabledIcon,
Grid,
SectionTitle,
ValuePair,
} from "mds";
import { twoColCssGridLayoutConfig } from "../../Common/FormComponents/common/styleLibrary";
import { IAM_SCOPES } from "../../../../common/SecureComponent/permissions";
import {
hasPermission,
SecureComponent,
} from "../../../../common/SecureComponent";
import {
selDistSet,
setErrorSnackMessage,
setHelpName,
} from "../../../../systemSlice";
import {
selBucketDetailsInfo,
selBucketDetailsLoading,
setBucketDetailsLoad,
} from "./bucketDetailsSlice";
import { useAppDispatch } from "../../../../store";
import VersioningInfo from "../VersioningInfo";
import withSuspense from "../../Common/Components/withSuspense";
import LabelWithIcon from "./SummaryItems/LabelWithIcon";
import EditablePropertyItem from "./SummaryItems/EditablePropertyItem";
import ReportedUsage from "./SummaryItems/ReportedUsage";
import BucketQuotaSize from "./SummaryItems/BucketQuotaSize";
const SetAccessPolicy = withSuspense(
React.lazy(() => import("./SetAccessPolicy")),
);
const SetRetentionConfig = withSuspense(
React.lazy(() => import("./SetRetentionConfig")),
);
const EnableBucketEncryption = withSuspense(
React.lazy(() => import("./EnableBucketEncryption")),
);
const EnableVersioningModal = withSuspense(
React.lazy(() => import("./EnableVersioningModal")),
);
const BucketTags = withSuspense(
React.lazy(() => import("./SummaryItems/BucketTags")),
);
const EnableQuota = withSuspense(React.lazy(() => import("./EnableQuota")));
const BucketSummary = () => {
const dispatch = useAppDispatch();
const params = useParams();
const loadingBucket = useSelector(selBucketDetailsLoading);
const bucketInfo = useSelector(selBucketDetailsInfo);
const distributedSetup = useSelector(selDistSet);
const [encryptionCfg, setEncryptionCfg] =
useState<BucketEncryptionInfo | null>(null);
const [bucketSize, setBucketSize] = useState<number | "0">("0");
const [hasObjectLocking, setHasObjectLocking] = useState<boolean | undefined>(
false,
);
const [accessPolicyScreenOpen, setAccessPolicyScreenOpen] =
useState<boolean>(false);
const [replicationRules, setReplicationRules] = useState<boolean>(false);
const [loadingObjectLocking, setLoadingLocking] = useState<boolean>(true);
const [loadingSize, setLoadingSize] = useState<boolean>(true);
const [bucketLoading, setBucketLoading] = useState<boolean>(true);
const [loadingEncryption, setLoadingEncryption] = useState<boolean>(true);
const [loadingVersioning, setLoadingVersioning] = useState<boolean>(true);
const [loadingQuota, setLoadingQuota] = useState<boolean>(true);
const [loadingReplication, setLoadingReplication] = useState<boolean>(true);
const [loadingRetention, setLoadingRetention] = useState<boolean>(true);
const [versioningInfo, setVersioningInfo] =
useState<BucketVersioningResponse>();
const [quotaEnabled, setQuotaEnabled] = useState<boolean>(false);
const [quota, setQuota] = useState<BucketQuota | null>(null);
const [encryptionEnabled, setEncryptionEnabled] = useState<boolean>(false);
const [retentionEnabled, setRetentionEnabled] = useState<boolean>(false);
const [retentionConfig, setRetentionConfig] =
useState<GetBucketRetentionConfig | null>(null);
const [retentionConfigOpen, setRetentionConfigOpen] =
useState<boolean>(false);
const [enableEncryptionScreenOpen, setEnableEncryptionScreenOpen] =
useState<boolean>(false);
const [enableQuotaScreenOpen, setEnableQuotaScreenOpen] =
useState<boolean>(false);
const [enableVersioningOpen, setEnableVersioningOpen] =
useState<boolean>(false);
useEffect(() => {
dispatch(setHelpName("bucket_detail_summary"));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const bucketName = params.bucketName || "";
let accessPolicy = "PRIVATE";
let policyDefinition = "";
if (bucketInfo !== null && bucketInfo.access && bucketInfo.definition) {
accessPolicy = bucketInfo.access;
policyDefinition = bucketInfo.definition;
}
const displayGetBucketObjectLockConfiguration = hasPermission(bucketName, [
IAM_SCOPES.S3_GET_BUCKET_OBJECT_LOCK_CONFIGURATION,
IAM_SCOPES.S3_GET_ACTIONS,
]);
const displayGetBucketEncryptionConfiguration = hasPermission(bucketName, [
IAM_SCOPES.S3_GET_BUCKET_ENCRYPTION_CONFIGURATION,
IAM_SCOPES.S3_GET_ACTIONS,
]);
const displayGetBucketQuota = hasPermission(bucketName, [
IAM_SCOPES.ADMIN_GET_BUCKET_QUOTA,
]);
useEffect(() => {
if (loadingBucket) {
setBucketLoading(true);
} else {
setBucketLoading(false);
}
}, [loadingBucket, setBucketLoading]);
useEffect(() => {
if (loadingEncryption) {
if (displayGetBucketEncryptionConfiguration) {
api.buckets
.getBucketEncryptionInfo(bucketName)
.then((res) => {
if (res.data.algorithm) {
setEncryptionEnabled(true);
setEncryptionCfg(res.data);
}
setLoadingEncryption(false);
})
.catch((err) => {
err = errorToHandler(err.error);
if (
err.errorMessage ===
"The server side encryption configuration was not found"
) {
setEncryptionEnabled(false);
setEncryptionCfg(null);
}
setLoadingEncryption(false);
});
} else {
setEncryptionEnabled(false);
setEncryptionCfg(null);
setLoadingEncryption(false);
}
}
}, [loadingEncryption, bucketName, displayGetBucketEncryptionConfiguration]);
useEffect(() => {
if (loadingVersioning && distributedSetup) {
api.buckets
.getBucketVersioning(bucketName)
.then((res) => {
setVersioningInfo(res.data);
setLoadingVersioning(false);
})
.catch((err) => {
dispatch(setErrorSnackMessage(errorToHandler(err.error)));
setLoadingVersioning(false);
});
}
}, [loadingVersioning, dispatch, bucketName, distributedSetup]);
useEffect(() => {
if (loadingQuota && distributedSetup) {
if (displayGetBucketQuota) {
api.buckets
.getBucketQuota(bucketName)
.then((res) => {
setQuota(res.data);
if (res.data.quota) {
setQuotaEnabled(true);
} else {
setQuotaEnabled(false);
}
setLoadingQuota(false);
})
.catch((err) => {
dispatch(setErrorSnackMessage(errorToHandler(err.error)));
setQuotaEnabled(false);
setLoadingQuota(false);
});
} else {
setQuotaEnabled(false);
setLoadingQuota(false);
}
}
}, [
loadingQuota,
setLoadingVersioning,
dispatch,
bucketName,
distributedSetup,
displayGetBucketQuota,
]);
useEffect(() => {
if (loadingVersioning && distributedSetup) {
if (displayGetBucketObjectLockConfiguration) {
api.buckets
.getBucketObjectLockingStatus(bucketName)
.then((res) => {
setHasObjectLocking(res.data.object_locking_enabled);
setLoadingLocking(false);
})
.catch((err) => {
dispatch(setErrorSnackMessage(errorToHandler(err.error)));
setLoadingLocking(false);
});
} else {
setLoadingLocking(false);
}
}
}, [
loadingObjectLocking,
dispatch,
bucketName,
loadingVersioning,
distributedSetup,
displayGetBucketObjectLockConfiguration,
]);
useEffect(() => {
if (loadingSize) {
api.buckets
.listBuckets()
.then((res) => {
const resBuckets = get(res.data, "buckets", []);
const bucketInfo = resBuckets.find(
(bucket) => bucket.name === bucketName,
);
const size = get(bucketInfo, "size", "0");
setLoadingSize(false);
setBucketSize(size);
})
.catch((err) => {
setLoadingSize(false);
dispatch(setErrorSnackMessage(errorToHandler(err.error)));
});
}
}, [loadingSize, dispatch, bucketName]);
useEffect(() => {
if (loadingReplication && distributedSetup) {
api.buckets
.getBucketReplication(bucketName)
.then((res) => {
const r = res.data.rules ? res.data.rules : [];
setReplicationRules(r.length > 0);
setLoadingReplication(false);
})
.catch((err) => {
dispatch(setErrorSnackMessage(errorToHandler(err.error)));
setLoadingReplication(false);
});
}
}, [loadingReplication, dispatch, bucketName, distributedSetup]);
useEffect(() => {
if (loadingRetention && hasObjectLocking) {
api.buckets
.getBucketRetentionConfig(bucketName)
.then((res) => {
setLoadingRetention(false);
setRetentionEnabled(true);
setRetentionConfig(res.data);
})
.catch((err) => {
setRetentionEnabled(false);
setLoadingRetention(false);
setRetentionConfig(null);
});
}
}, [loadingRetention, hasObjectLocking, bucketName]);
const loadAllBucketData = () => {
dispatch(setBucketDetailsLoad(true));
setBucketLoading(true);
setLoadingSize(true);
setLoadingVersioning(true);
setLoadingEncryption(true);
setLoadingRetention(true);
};
const setBucketVersioning = () => {
setEnableVersioningOpen(true);
};
const setBucketQuota = () => {
setEnableQuotaScreenOpen(true);
};
const closeEnableBucketEncryption = () => {
setEnableEncryptionScreenOpen(false);
setLoadingEncryption(true);
};
const closeEnableBucketQuota = () => {
setEnableQuotaScreenOpen(false);
setLoadingQuota(true);
};
const closeSetAccessPolicy = () => {
setAccessPolicyScreenOpen(false);
loadAllBucketData();
};
const closeRetentionConfig = () => {
setRetentionConfigOpen(false);
loadAllBucketData();
};
const closeEnableVersioning = (refresh: boolean) => {
setEnableVersioningOpen(false);
if (refresh) {
loadAllBucketData();
}
};
let versioningStatus = versioningInfo?.status;
let versioningText = "Unversioned (Default)";
if (versioningStatus === "Enabled") {
versioningText = "Versioned";
} else if (versioningStatus === "Suspended") {
versioningText = "Suspended";
}
return (
<Fragment>
{enableEncryptionScreenOpen && (
<EnableBucketEncryption
open={enableEncryptionScreenOpen}
selectedBucket={bucketName}
encryptionEnabled={encryptionEnabled}
encryptionCfg={encryptionCfg}
closeModalAndRefresh={closeEnableBucketEncryption}
/>
)}
{enableQuotaScreenOpen && (
<EnableQuota
open={enableQuotaScreenOpen}
selectedBucket={bucketName}
enabled={quotaEnabled}
cfg={quota}
closeModalAndRefresh={closeEnableBucketQuota}
/>
)}
{accessPolicyScreenOpen && (
<SetAccessPolicy
bucketName={bucketName}
open={accessPolicyScreenOpen}
actualPolicy={accessPolicy}
actualDefinition={policyDefinition}
closeModalAndRefresh={closeSetAccessPolicy}
/>
)}
{retentionConfigOpen && (
<SetRetentionConfig
bucketName={bucketName}
open={retentionConfigOpen}
closeModalAndRefresh={closeRetentionConfig}
/>
)}
{enableVersioningOpen && (
<EnableVersioningModal
closeVersioningModalAndRefresh={closeEnableVersioning}
modalOpen={enableVersioningOpen}
selectedBucket={bucketName}
versioningInfo={versioningInfo}
objectLockingEnabled={!!hasObjectLocking}
/>
)}
<SectionTitle separator sx={{ marginBottom: 15 }}>
Summary
</SectionTitle>
<Grid container>
<SecureComponent
scopes={[IAM_SCOPES.S3_GET_BUCKET_POLICY, IAM_SCOPES.S3_GET_ACTIONS]}
resource={bucketName}
>
<Grid item xs={12}>
<Box sx={twoColCssGridLayoutConfig}>
<Box sx={twoColCssGridLayoutConfig}>
<SecureComponent
scopes={[
IAM_SCOPES.S3_GET_BUCKET_POLICY,
IAM_SCOPES.S3_GET_ACTIONS,
]}
resource={bucketName}
>
<EditablePropertyItem
iamScopes={[
IAM_SCOPES.S3_PUT_BUCKET_POLICY,
IAM_SCOPES.S3_PUT_ACTIONS,
]}
resourceName={bucketName}
property={"Access Policy:"}
value={accessPolicy.toLowerCase()}
onEdit={() => {
setAccessPolicyScreenOpen(true);
}}
isLoading={bucketLoading}
helpTip={
<Fragment>
<strong>Private</strong> policy limits access to
credentialled accounts with appropriate permissions
<br />
<strong>Public</strong> policy anyone will be able to
upload, download and delete files from this Bucket once
logged in
<br />
<strong>Custom</strong> policy can be written to define
which accounts are authorized to access this Bucket
<br />
<br />
To allow Bucket access without credentials, use the{" "}
<a href={`/buckets/${bucketName}/admin/prefix`}>
Anonymous
</a>{" "}
setting
</Fragment>
}
/>
</SecureComponent>
<SecureComponent
scopes={[
IAM_SCOPES.S3_GET_BUCKET_ENCRYPTION_CONFIGURATION,
IAM_SCOPES.S3_GET_ACTIONS,
]}
resource={bucketName}
>
<EditablePropertyItem
iamScopes={[
IAM_SCOPES.S3_PUT_BUCKET_ENCRYPTION_CONFIGURATION,
IAM_SCOPES.S3_PUT_ACTIONS,
]}
resourceName={bucketName}
property={"Encryption:"}
value={encryptionEnabled ? "Enabled" : "Disabled"}
onEdit={() => {
setEnableEncryptionScreenOpen(true);
}}
isLoading={loadingEncryption}
helpTip={
<Fragment>
MinIO supports enabling automatic{" "}
<a
href="https://min.io/docs/minio/kubernetes/upstream/administration/server-side-encryption/server-side-encryption-sse-kms.html"
target="blank"
>
SSE-KMS
</a>{" "}
and{" "}
<a
href="https://min.io/docs/minio/kubernetes/upstream/administration/server-side-encryption/server-side-encryption-sse-s3.html"
target="blank"
>
SSE-S3
</a>{" "}
encryption of all objects written to a bucket using a
specific External Key (EK) stored on the external KMS.
</Fragment>
}
/>
</SecureComponent>
<SecureComponent
scopes={[
IAM_SCOPES.S3_GET_REPLICATION_CONFIGURATION,
IAM_SCOPES.S3_GET_ACTIONS,
]}
resource={bucketName}
>
<ValuePair
label={"Replication:"}
value={
<LabelWithIcon
icon={
replicationRules ? <EnabledIcon /> : <DisabledIcon />
}
label={
<label className={"muted"}>
{replicationRules ? "Enabled" : "Disabled"}
</label>
}
/>
}
/>
</SecureComponent>
<SecureComponent
scopes={[
IAM_SCOPES.S3_GET_BUCKET_OBJECT_LOCK_CONFIGURATION,
IAM_SCOPES.S3_GET_ACTIONS,
]}
resource={bucketName}
>
<ValuePair
label={"Object Locking:"}
value={
<LabelWithIcon
icon={
hasObjectLocking ? <EnabledIcon /> : <DisabledIcon />
}
label={
<label className={"muted"}>
{hasObjectLocking ? "Enabled" : "Disabled"}
</label>
}
/>
}
/>
</SecureComponent>
<Box>
<ValuePair
label={"Tags:"}
value={<BucketTags bucketName={bucketName} />}
/>
</Box>
<EditablePropertyItem
iamScopes={[IAM_SCOPES.ADMIN_SET_BUCKET_QUOTA]}
resourceName={bucketName}
property={"Quota:"}
value={quotaEnabled ? "Enabled" : "Disabled"}
onEdit={setBucketQuota}
isLoading={loadingQuota}
helpTip={
<Fragment>
Setting a{" "}
<a
href="https://min.io/docs/minio/linux/reference/minio-mc/mc-quota-set.html"
target="blank"
>
quota
</a>{" "}
assigns a hard limit to a bucket beyond which MinIO does
not allow writes.
</Fragment>
}
/>
</Box>
<Box
sx={{
display: "grid",
gridTemplateColumns: "1fr",
alignItems: "flex-start",
}}
>
<ReportedUsage bucketSize={`${bucketSize}`} />
{quotaEnabled && quota ? (
<BucketQuotaSize quota={quota} />
) : null}
</Box>
</Box>
</Grid>
</SecureComponent>
{distributedSetup && (
<SecureComponent
scopes={[
IAM_SCOPES.S3_GET_BUCKET_VERSIONING,
IAM_SCOPES.S3_GET_ACTIONS,
]}
resource={bucketName}
>
<Grid item xs={12} sx={{ marginTop: 5 }}>
<SectionTitle separator sx={{ marginBottom: 15 }}>
Versioning
</SectionTitle>
<Box sx={twoColCssGridLayoutConfig}>
<Box sx={twoColCssGridLayoutConfig}>
<EditablePropertyItem
iamScopes={[
IAM_SCOPES.S3_PUT_BUCKET_VERSIONING,
IAM_SCOPES.S3_PUT_ACTIONS,
]}
resourceName={bucketName}
property={"Current Status:"}
value={
<Box
sx={{
display: "flex",
flexDirection: "column",
textDecorationStyle: "initial",
placeItems: "flex-start",
justifyItems: "flex-start",
gap: 3,
}}
>
<div> {versioningText}</div>
</Box>
}
onEdit={setBucketVersioning}
isLoading={loadingVersioning}
disabled={hasObjectLocking}
/>
{versioningInfo?.status === "Enabled" ? (
<VersioningInfo versioningState={versioningInfo} />
) : null}
</Box>
</Box>
</Grid>
</SecureComponent>
)}
{hasObjectLocking && (
<SecureComponent
scopes={[
IAM_SCOPES.S3_GET_OBJECT_RETENTION,
IAM_SCOPES.S3_GET_ACTIONS,
]}
resource={bucketName}
>
<Grid item xs={12} sx={{ marginTop: 5 }}>
<SectionTitle separator sx={{ marginBottom: 15 }}>
Retention
</SectionTitle>
<Box sx={twoColCssGridLayoutConfig}>
<Box sx={twoColCssGridLayoutConfig}>
<EditablePropertyItem
iamScopes={[IAM_SCOPES.ADMIN_SET_BUCKET_QUOTA]}
resourceName={bucketName}
property={"Retention:"}
value={retentionEnabled ? "Enabled" : "Disabled"}
onEdit={() => {
setRetentionConfigOpen(true);
}}
isLoading={loadingRetention}
helpTip={
<Fragment>
MinIO{" "}
<a
target="blank"
href="https://min.io/docs/minio/macos/administration/object-management.html#object-retention"
>
Object Locking
</a>{" "}
enforces Write-Once Read-Many (WORM) immutability to
protect versioned objects from deletion.
</Fragment>
}
/>
<ValuePair
label={"Mode:"}
value={
<label
className={"muted"}
style={{ textTransform: "capitalize" }}
>
{retentionConfig && retentionConfig.mode
? retentionConfig.mode
: "-"}
</label>
}
/>
<ValuePair
label={"Validity:"}
value={
<label
className={"muted"}
style={{ textTransform: "capitalize" }}
>
{retentionConfig && retentionConfig.validity}{" "}
{retentionConfig &&
(retentionConfig.validity === 1
? retentionConfig.unit?.slice(0, -1)
: retentionConfig.unit)}
</label>
}
/>
</Box>
</Box>
</Grid>
</SecureComponent>
)}
</Grid>
</Fragment>
);
};
export default BucketSummary;

View File

@@ -1,75 +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, useState } from "react";
import { ConfirmDeleteIcon } from "mds";
import { setErrorSnackMessage } from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import { api } from "api";
import { ApiError, HttpResponse, PrefixWrapper } from "api/consoleApi";
import { errorToHandler } from "api/errors";
import ConfirmDialog from "../../Common/ModalWrapper/ConfirmDialog";
interface IDeleteAccessRule {
modalOpen: boolean;
onClose: () => any;
bucket: string;
toDelete: string;
}
const DeleteAccessRule = ({
onClose,
modalOpen,
bucket,
toDelete,
}: IDeleteAccessRule) => {
const dispatch = useAppDispatch();
const [loadingDeleteAccessRule, setLoadingDeleteAccessRule] =
useState<boolean>(false);
const onConfirmDelete = () => {
setLoadingDeleteAccessRule(true);
let wrapper: PrefixWrapper = { prefix: toDelete };
api.bucket
.deleteAccessRuleWithBucket(bucket, wrapper)
.then(() => {
onClose();
})
.catch((res: HttpResponse<boolean, ApiError>) => {
dispatch(setErrorSnackMessage(errorToHandler(res.error)));
onClose();
})
.finally(() => setLoadingDeleteAccessRule(false));
};
return (
<ConfirmDialog
title={`Delete Anonymous Access Rule`}
confirmText={"Delete"}
isOpen={modalOpen}
isLoading={loadingDeleteAccessRule}
onConfirm={onConfirmDelete}
titleIcon={<ConfirmDeleteIcon />}
onClose={onClose}
confirmationContent={
<Fragment>Are you sure you want to delete this access rule?</Fragment>
}
/>
);
};
export default DeleteAccessRule;

View File

@@ -1,91 +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 } from "react";
import useApi from "../../Common/Hooks/useApi";
import ConfirmDialog from "../../Common/ModalWrapper/ConfirmDialog";
import { ErrorResponseHandler } from "../../../../common/types";
import { ConfirmDeleteIcon } from "mds";
import { setErrorSnackMessage } from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
interface IDeleteBucketTagModal {
deleteOpen: boolean;
currentTags: any;
bucketName: string;
selectedTag: string[];
onCloseAndUpdate: (refresh: boolean) => void;
}
const DeleteBucketTagModal = ({
deleteOpen,
currentTags,
selectedTag,
onCloseAndUpdate,
bucketName,
}: IDeleteBucketTagModal) => {
const dispatch = useAppDispatch();
const [tagKey, tagLabel] = selectedTag;
const onDelSuccess = () => onCloseAndUpdate(true);
const onDelError = (err: ErrorResponseHandler) =>
dispatch(setErrorSnackMessage(err));
const onClose = () => onCloseAndUpdate(false);
const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError);
if (!selectedTag) {
return null;
}
const onConfirmDelete = () => {
const cleanObject = { ...currentTags };
delete cleanObject[tagKey];
invokeDeleteApi("PUT", `/api/v1/buckets/${bucketName}/tags`, {
tags: cleanObject,
});
};
return (
<ConfirmDialog
title={`Delete Tag`}
confirmText={"Delete"}
isOpen={deleteOpen}
titleIcon={<ConfirmDeleteIcon />}
isLoading={deleteLoading}
onConfirm={onConfirmDelete}
onClose={onClose}
confirmationContent={
<Fragment>
Are you sure you want to delete the tag{" "}
<b
style={{
maxWidth: 200,
whiteSpace: "normal",
wordWrap: "break-word",
}}
>
{tagKey} : {tagLabel}
</b>{" "}
?
</Fragment>
}
/>
);
};
export default DeleteBucketTagModal;

View File

@@ -1,95 +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 } from "react";
import get from "lodash/get";
import { ErrorResponseHandler } from "../../../../common/types";
import useApi from "../../Common/Hooks/useApi";
import ConfirmDialog from "../../Common/ModalWrapper/ConfirmDialog";
import { ConfirmDeleteIcon } from "mds";
import { setErrorSnackMessage } from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import { NotificationConfig } from "api/consoleApi";
interface IDeleteEventProps {
closeDeleteModalAndRefresh: (refresh: boolean) => void;
deleteOpen: boolean;
selectedBucket: string;
bucketEvent: NotificationConfig | null;
}
const DeleteEvent = ({
closeDeleteModalAndRefresh,
deleteOpen,
selectedBucket,
bucketEvent,
}: IDeleteEventProps) => {
const dispatch = useAppDispatch();
const onDelSuccess = () => closeDeleteModalAndRefresh(true);
const onDelError = (err: ErrorResponseHandler) =>
dispatch(setErrorSnackMessage(err));
const onClose = () => closeDeleteModalAndRefresh(false);
const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError);
if (!selectedBucket) {
return null;
}
const onConfirmDelete = () => {
if (bucketEvent === null) {
return;
}
const events: string[] = get(bucketEvent, "events", []);
const prefix = get(bucketEvent, "prefix", "");
const suffix = get(bucketEvent, "suffix", "");
const cleanEvents = events.reduce((acc: string[], currVal: string) => {
if (!acc.includes(currVal)) {
return [...acc, currVal];
}
return acc;
}, []);
invokeDeleteApi(
"DELETE",
`/api/v1/buckets/${selectedBucket}/events/${bucketEvent.arn}`,
{
events: cleanEvents,
prefix,
suffix,
},
);
};
return (
<ConfirmDialog
title={`Delete Event`}
confirmText={"Delete"}
isOpen={deleteOpen}
titleIcon={<ConfirmDeleteIcon />}
isLoading={deleteLoading}
onConfirm={onConfirmDelete}
onClose={onClose}
confirmationContent={
<Fragment>Are you sure you want to delete this event?</Fragment>
}
/>
);
};
export default DeleteEvent;

View File

@@ -1,126 +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, useState } from "react";
import { ConfirmDeleteIcon, Grid, InputBox } from "mds";
import { ErrorResponseHandler } from "../../../../common/types";
import { setErrorSnackMessage } from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import useApi from "../../Common/Hooks/useApi";
import ConfirmDialog from "../../Common/ModalWrapper/ConfirmDialog";
interface IDeleteReplicationProps {
closeDeleteModalAndRefresh: (refresh: boolean) => void;
deleteOpen: boolean;
selectedBucket: string;
ruleToDelete?: string;
rulesToDelete?: string[];
remainingRules: number;
allSelected: boolean;
deleteSelectedRules?: boolean;
}
const DeleteReplicationRule = ({
closeDeleteModalAndRefresh,
deleteOpen,
selectedBucket,
ruleToDelete,
rulesToDelete,
remainingRules,
allSelected,
deleteSelectedRules = false,
}: IDeleteReplicationProps) => {
const dispatch = useAppDispatch();
const [confirmationText, setConfirmationText] = useState<string>("");
const onDelSuccess = () => closeDeleteModalAndRefresh(true);
const onDelError = (err: ErrorResponseHandler) =>
dispatch(setErrorSnackMessage(err));
const onClose = () => closeDeleteModalAndRefresh(false);
const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError);
if (!selectedBucket) {
return null;
}
const onConfirmDelete = () => {
let url = `/api/v1/buckets/${selectedBucket}/replication/${ruleToDelete}`;
if (deleteSelectedRules) {
if (allSelected) {
url = `/api/v1/buckets/${selectedBucket}/delete-all-replication-rules`;
} else {
url = `/api/v1/buckets/${selectedBucket}/delete-selected-replication-rules`;
invokeDeleteApi("DELETE", url, { rules: rulesToDelete });
return;
}
} else if (remainingRules === 1) {
url = `/api/v1/buckets/${selectedBucket}/delete-all-replication-rules`;
}
invokeDeleteApi("DELETE", url);
};
return (
<ConfirmDialog
title={
deleteSelectedRules
? "Delete Selected Replication Rules"
: "Delete Replication Rule"
}
confirmText={"Delete"}
isOpen={deleteOpen}
titleIcon={<ConfirmDeleteIcon />}
isLoading={deleteLoading}
onConfirm={onConfirmDelete}
onClose={onClose}
confirmButtonProps={{
disabled: deleteSelectedRules && confirmationText !== "Yes, I am sure",
}}
confirmationContent={
<Fragment>
{deleteSelectedRules ? (
<Fragment>
Are you sure you want to remove the selected replication rules for
bucket <b>{selectedBucket}</b>?<br />
<br />
To continue please type <b>Yes, I am sure</b> in the box.
<Grid item xs={12}>
<InputBox
id="retype-tenant"
name="retype-tenant"
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setConfirmationText(event.target.value);
}}
label=""
value={confirmationText}
/>
</Grid>
</Fragment>
) : (
<Fragment>
Are you sure you want to delete replication rule{" "}
<b>{ruleToDelete}</b>?
</Fragment>
)}
</Fragment>
}
/>
);
};
export default DeleteReplicationRule;

View File

@@ -1,111 +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, useState } from "react";
import { AddAccessRuleIcon, Button, FormLayout, Grid, Select } from "mds";
import { api } from "api";
import { errorToHandler } from "api/errors";
import { modalStyleUtils } from "../../Common/FormComponents/common/styleLibrary";
import { setErrorSnackMessage } from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
interface IEditAccessRule {
modalOpen: boolean;
onClose: () => any;
bucket: string;
toEdit: string;
initial: string;
}
const EditAccessRule = ({
modalOpen,
onClose,
bucket,
toEdit,
initial,
}: IEditAccessRule) => {
const dispatch = useAppDispatch();
const [selectedAccess, setSelectedAccess] = useState<any>(initial);
const accessOptions = [
{ label: "readonly", value: "readonly" },
{ label: "writeonly", value: "writeonly" },
{ label: "readwrite", value: "readwrite" },
];
const resetForm = () => {
setSelectedAccess(initial);
};
const createProcess = () => {
api.bucket
.setAccessRuleWithBucket(bucket, {
prefix: toEdit,
access: selectedAccess,
})
.then(() => {
onClose();
})
.catch((err) => {
dispatch(setErrorSnackMessage(errorToHandler(err.error)));
onClose();
});
};
return (
<Fragment>
<ModalWrapper
modalOpen={modalOpen}
title={`Edit Anonymous Access Rule for ${`${bucket}/${toEdit || ""}`}`}
onClose={onClose}
titleIcon={<AddAccessRuleIcon />}
>
<FormLayout containerPadding={false} withBorders={false}>
<Select
id="access"
name="Access"
onChange={(value) => {
setSelectedAccess(value);
}}
label="Access"
value={selectedAccess}
options={accessOptions}
disabled={false}
/>
</FormLayout>
<Grid item xs={12} sx={modalStyleUtils.modalButtonBar}>
<Button
id={"clear"}
type="button"
variant="regular"
onClick={resetForm}
label={"Clear"}
/>
<Button
id={"save"}
type="submit"
variant="callAction"
onClick={createProcess}
label={"Save"}
/>
</Grid>
</ModalWrapper>
</Fragment>
);
};
export default EditAccessRule;

View File

@@ -1,335 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2023 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import {
BackLink,
Box,
BucketReplicationIcon,
Button,
FormLayout,
Grid,
HelpBox,
InputBox,
PageLayout,
ReadBox,
Switch,
} from "mds";
import { IAM_PAGES } from "../../../../common/SecureComponent/permissions";
import { setErrorSnackMessage, setHelpName } from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import PageHeaderWrapper from "../../Common/PageHeaderWrapper/PageHeaderWrapper";
import HelpMenu from "../../HelpMenu";
import { api } from "api";
import { errorToHandler } from "api/errors";
import QueryMultiSelector from "screens/Console/Common/FormComponents/QueryMultiSelector/QueryMultiSelector";
const EditBucketReplication = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
let params = new URLSearchParams(document.location.search);
const bucketName = params.get("bucketName") || "";
const ruleID = params.get("ruleID") || "";
useEffect(() => {
dispatch(setHelpName("bucket-replication-edit"));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const backLink = IAM_PAGES.BUCKETS + `/${bucketName}/admin/replication`;
const [editLoading, setEditLoading] = useState<boolean>(true);
const [saveEdit, setSaveEdit] = useState<boolean>(false);
const [priority, setPriority] = useState<string>("1");
const [destination, setDestination] = useState<string>("");
const [prefix, setPrefix] = useState<string>("");
const [repDeleteMarker, setRepDeleteMarker] = useState<boolean>(false);
const [metadataSync, setMetadataSync] = useState<boolean>(false);
const [initialTags, setInitialTags] = useState<string>("");
const [tags, setTags] = useState<string>("");
const [targetStorageClass, setTargetStorageClass] = useState<string>("");
const [repExisting, setRepExisting] = useState<boolean>(false);
const [repDelete, setRepDelete] = useState<boolean>(false);
const [ruleState, setRuleState] = useState<boolean>(false);
useEffect(() => {
if (editLoading && bucketName && ruleID) {
api.buckets
.getBucketReplicationRule(bucketName, ruleID)
.then((res) => {
setPriority(res.data.priority ? res.data.priority.toString() : "");
const pref = res.data.prefix || "";
const tag = res.data.tags || "";
setPrefix(pref);
setInitialTags(tag);
setTags(tag);
setDestination(res.data.destination?.bucket || "");
setRepDeleteMarker(res.data.delete_marker_replication || false);
setTargetStorageClass(res.data.storageClass || "");
setRepExisting(!!res.data.existingObjects);
setRepDelete(!!res.data.deletes_replication);
setRuleState(res.data.status === "Enabled");
setMetadataSync(!!res.data.metadata_replication);
setEditLoading(false);
})
.catch((err) => {
dispatch(setErrorSnackMessage(errorToHandler(err.error)));
setEditLoading(false);
});
}
}, [editLoading, dispatch, bucketName, ruleID]);
useEffect(() => {
if (saveEdit && bucketName && ruleID) {
const remoteBucketsInfo = {
arn: destination,
ruleState: ruleState,
prefix: prefix,
tags: tags,
replicateDeleteMarkers: repDeleteMarker,
replicateDeletes: repDelete,
replicateExistingObjects: repExisting,
replicateMetadata: metadataSync,
priority: parseInt(priority),
storageClass: targetStorageClass,
};
api.buckets
.updateMultiBucketReplication(bucketName, ruleID, remoteBucketsInfo)
.then(() => {
navigate(backLink);
})
.catch((err) => {
dispatch(setErrorSnackMessage(errorToHandler(err.error)));
setSaveEdit(false);
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
saveEdit,
bucketName,
ruleID,
destination,
prefix,
tags,
repDeleteMarker,
priority,
repDelete,
repExisting,
ruleState,
metadataSync,
targetStorageClass,
dispatch,
]);
return (
<Fragment>
<PageHeaderWrapper
label={
<BackLink
label={"Edit Bucket Replication"}
onClick={() => navigate(backLink)}
/>
}
actions={<HelpMenu />}
/>
<PageLayout>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setSaveEdit(true);
}}
>
<FormLayout
containerPadding={false}
withBorders={false}
helpBox={
<HelpBox
iconComponent={<BucketReplicationIcon />}
title="Bucket Replication Configuration"
help={
<Fragment>
<Box sx={{ paddingTop: "10px" }}>
For each write operation to the bucket, MinIO checks all
configured replication rules for the bucket and applies
the matching rule with highest configured priority.
</Box>
<Box sx={{ paddingTop: "10px" }}>
MinIO supports enabling replication of existing objects in
a bucket.
</Box>
<Box sx={{ paddingTop: "10px" }}>
MinIO does not enable existing object replication by
default. Objects created before replication was configured
or while replication is disabled are not synchronized to
the target deployment unless replication of existing
objects is enabled.
</Box>
<Box sx={{ paddingTop: "10px" }}>
MinIO supports replicating delete operations, where MinIO
synchronizes deleting specific object versions and new
delete markers. Delete operation replication uses the same
replication process as all other replication operations.
</Box>{" "}
</Fragment>
}
/>
}
>
<Switch
checked={ruleState}
id="ruleState"
name="ruleState"
label="Rule State"
onChange={(e) => {
setRuleState(e.target.checked);
}}
/>
<ReadBox label={"Destination"} sx={{ width: "100%" }}>
{destination}
</ReadBox>
<InputBox
id="priority"
name="priority"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.validity.valid) {
setPriority(e.target.value);
}
}}
label="Priority"
value={priority}
pattern={"[0-9]*"}
/>
<InputBox
id="storageClass"
name="storageClass"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTargetStorageClass(e.target.value);
}}
placeholder="STANDARD_IA,REDUCED_REDUNDANCY etc"
label="Storage Class"
value={targetStorageClass}
/>
<fieldset className={"inputItem"}>
<legend>Object Filters</legend>
<InputBox
id="prefix"
name="prefix"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setPrefix(e.target.value);
}}
placeholder="prefix"
label="Prefix"
value={prefix}
/>
<QueryMultiSelector
name="tags"
label="Tags"
elements={initialTags}
onChange={(vl: string) => {
setTags(vl);
}}
keyPlaceholder="Tag Key"
valuePlaceholder="Tag Value"
withBorder
/>
</fieldset>
<fieldset className={"inputItem"}>
<legend>Replication Options</legend>
<Switch
checked={repExisting}
id="repExisting"
name="repExisting"
label="Existing Objects"
onChange={(e) => {
setRepExisting(e.target.checked);
}}
description={"Replicate existing objects"}
/>
<Switch
checked={metadataSync}
id="metadatataSync"
name="metadatataSync"
label="Metadata Sync"
onChange={(e) => {
setMetadataSync(e.target.checked);
}}
description={"Metadata Sync"}
/>
<Switch
checked={repDeleteMarker}
id="deleteMarker"
name="deleteMarker"
label="Delete Marker"
onChange={(e) => {
setRepDeleteMarker(e.target.checked);
}}
description={"Replicate soft deletes"}
/>
<Switch
checked={repDelete}
id="repDelete"
name="repDelete"
label="Deletes"
onChange={(e) => {
setRepDelete(e.target.checked);
}}
description={"Replicate versioned deletes"}
/>
</fieldset>
<Grid
item
xs={12}
sx={{
display: "flex",
flexDirection: "row",
justifyContent: "end",
gap: 10,
paddingTop: 10,
}}
>
<Button
id={"cancel-edit-replication"}
type="button"
variant="regular"
disabled={editLoading || saveEdit}
onClick={() => {
navigate(backLink);
}}
label={"Cancel"}
/>
<Button
id={"save-replication"}
type="submit"
variant="callAction"
disabled={editLoading || saveEdit}
label={"Save"}
/>
</Grid>
</FormLayout>
</form>
</PageLayout>
</Fragment>
);
};
export default EditBucketReplication;

View File

@@ -1,283 +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, { useEffect, useState } from "react";
import {
BucketReplicationIcon,
Button,
FormLayout,
InputBox,
ReadBox,
Switch,
Grid,
} from "mds";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import QueryMultiSelector from "../../Common/FormComponents/QueryMultiSelector/QueryMultiSelector";
import { modalStyleUtils } from "../../Common/FormComponents/common/styleLibrary";
import { setModalErrorSnackMessage } from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import { api } from "api";
import { errorToHandler } from "api/errors";
interface IEditReplicationModal {
closeModalAndRefresh: (refresh: boolean) => void;
open: boolean;
bucketName: string;
ruleID: string;
}
const EditReplicationModal = ({
closeModalAndRefresh,
open,
bucketName,
ruleID,
}: IEditReplicationModal) => {
const dispatch = useAppDispatch();
const [editLoading, setEditLoading] = useState<boolean>(true);
const [saveEdit, setSaveEdit] = useState<boolean>(false);
const [priority, setPriority] = useState<string>("1");
const [destination, setDestination] = useState<string>("");
const [prefix, setPrefix] = useState<string>("");
const [repDeleteMarker, setRepDeleteMarker] = useState<boolean>(false);
const [metadataSync, setMetadataSync] = useState<boolean>(false);
const [initialTags, setInitialTags] = useState<string>("");
const [tags, setTags] = useState<string>("");
const [targetStorageClass, setTargetStorageClass] = useState<string>("");
const [repExisting, setRepExisting] = useState<boolean>(false);
const [repDelete, setRepDelete] = useState<boolean>(false);
const [ruleState, setRuleState] = useState<boolean>(false);
useEffect(() => {
if (editLoading) {
api.buckets
.getBucketReplicationRule(bucketName, ruleID)
.then((res) => {
setPriority(res.data.priority ? res.data.priority.toString() : "");
const pref = res.data.prefix || "";
const tag = res.data.tags || "";
setPrefix(pref);
setInitialTags(tag);
setTags(tag);
setDestination(res.data.destination?.bucket || "");
setRepDeleteMarker(res.data.delete_marker_replication || false);
setTargetStorageClass(res.data.storageClass || "");
setRepExisting(!!res.data.existingObjects);
setRepDelete(!!res.data.deletes_replication);
setRuleState(res.data.status === "Enabled");
setMetadataSync(!!res.data.metadata_replication);
setEditLoading(false);
})
.catch((err) => {
dispatch(setModalErrorSnackMessage(errorToHandler(err.error)));
setEditLoading(false);
});
}
}, [editLoading, dispatch, bucketName, ruleID]);
useEffect(() => {
if (saveEdit) {
const remoteBucketsInfo = {
arn: destination,
ruleState: ruleState,
prefix: prefix,
tags: tags,
replicateDeleteMarkers: repDeleteMarker,
replicateDeletes: repDelete,
replicateExistingObjects: repExisting,
replicateMetadata: metadataSync,
priority: parseInt(priority),
storageClass: targetStorageClass,
};
api.buckets
.updateMultiBucketReplication(bucketName, ruleID, remoteBucketsInfo)
.then(() => {
setSaveEdit(false);
closeModalAndRefresh(true);
})
.catch((err) => {
dispatch(setModalErrorSnackMessage(errorToHandler(err.error)));
setSaveEdit(false);
});
}
}, [
saveEdit,
bucketName,
ruleID,
destination,
prefix,
tags,
repDeleteMarker,
priority,
repDelete,
repExisting,
ruleState,
metadataSync,
targetStorageClass,
closeModalAndRefresh,
dispatch,
]);
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
closeModalAndRefresh(false);
}}
title="Edit Bucket Replication"
titleIcon={<BucketReplicationIcon />}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setSaveEdit(true);
}}
>
<FormLayout containerPadding={false} withBorders={false}>
<Switch
checked={ruleState}
id="ruleState"
name="ruleState"
label="Rule State"
onChange={(e) => {
setRuleState(e.target.checked);
}}
/>
<ReadBox label={"Destination"} sx={{ width: "100%" }}>
{destination}
</ReadBox>
<InputBox
id="priority"
name="priority"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.validity.valid) {
setPriority(e.target.value);
}
}}
label="Priority"
value={priority}
pattern={"[0-9]*"}
/>
<InputBox
id="storageClass"
name="storageClass"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTargetStorageClass(e.target.value);
}}
placeholder="STANDARD_IA,REDUCED_REDUNDANCY etc"
label="Storage Class"
value={targetStorageClass}
/>
<fieldset className={"inputItem"}>
<legend>Object Filters</legend>
<InputBox
id="prefix"
name="prefix"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setPrefix(e.target.value);
}}
placeholder="prefix"
label="Prefix"
value={prefix}
/>
<QueryMultiSelector
name="tags"
label="Tags"
elements={initialTags}
onChange={(vl: string) => {
setTags(vl);
}}
keyPlaceholder="Tag Key"
valuePlaceholder="Tag Value"
withBorder
/>
</fieldset>
<fieldset className={"inputItem"}>
<legend>Replication Options</legend>
<Switch
checked={repExisting}
id="repExisting"
name="repExisting"
label="Existing Objects"
onChange={(e) => {
setRepExisting(e.target.checked);
}}
description={"Replicate existing objects"}
/>
<Switch
checked={metadataSync}
id="metadatataSync"
name="metadatataSync"
label="Metadata Sync"
onChange={(e) => {
setMetadataSync(e.target.checked);
}}
description={"Metadata Sync"}
/>
<Switch
checked={repDeleteMarker}
id="deleteMarker"
name="deleteMarker"
label="Delete Marker"
onChange={(e) => {
setRepDeleteMarker(e.target.checked);
}}
description={"Replicate soft deletes"}
/>
<Switch
checked={repDelete}
id="repDelete"
name="repDelete"
label="Deletes"
onChange={(e) => {
setRepDelete(e.target.checked);
}}
description={"Replicate versioned deletes"}
/>
</fieldset>
<Grid item xs={12} sx={modalStyleUtils.modalButtonBar}>
<Button
id={"cancel-edit-replication"}
type="button"
variant="regular"
disabled={editLoading || saveEdit}
onClick={() => {
closeModalAndRefresh(false);
}}
label={"Cancel"}
/>
<Button
id={"save-replication"}
type="submit"
variant="callAction"
disabled={editLoading || saveEdit}
label={"Save"}
/>
</Grid>
</FormLayout>
</form>
</ModalWrapper>
);
};
export default EditReplicationModal;

View File

@@ -1,255 +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 {
AddIcon,
Box,
BucketEncryptionIcon,
Button,
FormLayout,
Grid,
ProgressBar,
Select,
} from "mds";
import {
ApiError,
BucketEncryptionInfo,
BucketEncryptionType,
KmsKeyInfo,
} from "api/consoleApi";
import { api } from "api";
import { errorToHandler } from "api/errors";
import { modalStyleUtils } from "../../Common/FormComponents/common/styleLibrary";
import { setModalErrorSnackMessage } from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import {
CONSOLE_UI_RESOURCE,
IAM_SCOPES,
} from "../../../../common/SecureComponent/permissions";
import { SecureComponent } from "../../../../common/SecureComponent";
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
import AddKeyModal from "./AddKeyModal";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
interface IEnableBucketEncryptionProps {
open: boolean;
encryptionEnabled: boolean;
encryptionCfg: BucketEncryptionInfo | null;
selectedBucket: string;
closeModalAndRefresh: () => void;
}
const EnableBucketEncryption = ({
open,
encryptionCfg,
selectedBucket,
closeModalAndRefresh,
}: IEnableBucketEncryptionProps) => {
const dispatch = useAppDispatch();
const [loading, setLoading] = useState<boolean>(false);
const [kmsKeyID, setKmsKeyID] = useState<string>("");
const [encryptionType, setEncryptionType] = useState<
BucketEncryptionType | "disabled"
>("disabled");
const [keys, setKeys] = useState<KmsKeyInfo[] | undefined>([]);
const [loadingKeys, setLoadingKeys] = useState<boolean>(false);
const [addOpen, setAddOpen] = useState<boolean>(false);
useEffect(() => {
if (encryptionCfg) {
if (encryptionCfg.algorithm === "AES256") {
setEncryptionType(BucketEncryptionType.SseS3);
} else {
setEncryptionType(BucketEncryptionType.SseKms);
setKmsKeyID(encryptionCfg.kmsMasterKeyID || "");
}
}
}, [encryptionCfg]);
useEffect(() => {
if (encryptionType === "sse-kms") {
api.kms
.kmsListKeys()
.then((res) => {
setKeys(res.data.results);
setLoadingKeys(false);
})
.catch((err) => {
setLoadingKeys(false);
dispatch(setModalErrorSnackMessage(errorToHandler(err.error)));
});
}
}, [encryptionType, loadingKeys, dispatch]);
const enableBucketEncryption = (event: React.FormEvent) => {
event.preventDefault();
if (loading) {
return;
}
if (encryptionType === "disabled") {
api.buckets
.disableBucketEncryption(selectedBucket)
.then(() => {
setLoading(false);
closeModalAndRefresh();
})
.catch(async (res) => {
const err = (await res.json()) as ApiError;
setLoading(false);
dispatch(setModalErrorSnackMessage(errorToHandler(err)));
});
} else {
api.buckets
.enableBucketEncryption(selectedBucket, {
encType: encryptionType,
kmsKeyID: kmsKeyID,
})
.then(() => {
setLoading(false);
closeModalAndRefresh();
})
.catch(async (res) => {
const err = (await res.json()) as ApiError;
setLoading(false);
dispatch(setModalErrorSnackMessage(errorToHandler(err)));
});
}
};
return (
<Fragment>
{addOpen && (
<AddKeyModal
addOpen={addOpen}
closeAddModalAndRefresh={(refresh: boolean) => {
setAddOpen(false);
setLoadingKeys(true);
}}
/>
)}
<ModalWrapper
modalOpen={open}
onClose={() => {
closeModalAndRefresh();
}}
title="Enable Bucket Encryption"
titleIcon={<BucketEncryptionIcon />}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
enableBucketEncryption(e);
}}
>
<FormLayout withBorders={false} containerPadding={false}>
<Select
onChange={(value) => {
setEncryptionType(value as BucketEncryptionType | "disabled");
}}
id="select-encryption-type"
name="select-encryption-type"
label={"Encryption Type"}
value={encryptionType}
options={[
{
label: "Disabled",
value: "disabled",
},
{
label: "SSE-S3",
value: BucketEncryptionType.SseS3,
},
{
label: "SSE-KMS",
value: BucketEncryptionType.SseKms,
},
]}
/>
{encryptionType === "sse-kms" && (
<Box sx={{ display: "flex", gap: 10 }} className={"inputItem"}>
{keys && (
<Select
onChange={(value) => {
setKmsKeyID(value);
}}
id="select-kms-key-id"
name="select-kms-key-id"
label={"KMS Key ID"}
value={kmsKeyID}
options={keys.map((key: KmsKeyInfo) => {
return {
label: key.name || "",
value: key.name || "",
};
})}
/>
)}
<SecureComponent
scopes={[IAM_SCOPES.KMS_IMPORT_KEY]}
resource={CONSOLE_UI_RESOURCE}
errorProps={{ disabled: true }}
>
<TooltipWrapper tooltip={"Add key"}>
<Button
id={"import-key"}
variant={"regular"}
icon={<AddIcon />}
onClick={(e) => {
setAddOpen(true);
e.preventDefault();
}}
/>
</TooltipWrapper>
</SecureComponent>
</Box>
)}
<Grid item xs={12} sx={modalStyleUtils.modalButtonBar}>
<Button
id={"cancel"}
type="submit"
variant="regular"
onClick={() => {
closeModalAndRefresh();
}}
disabled={loading}
label={"Cancel"}
/>
<Button
id={"save"}
type="submit"
variant="callAction"
disabled={loading}
label={"Save"}
/>
</Grid>
{loading && (
<Grid item xs={12}>
<ProgressBar />
</Grid>
)}
</FormLayout>
</form>
</ModalWrapper>
</Fragment>
);
};
export default EnableBucketEncryption;

View File

@@ -1,199 +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, { useEffect, useState } from "react";
import {
BucketQuotaIcon,
Button,
FormLayout,
InputBox,
Switch,
Grid,
ProgressBar,
} from "mds";
import {
calculateBytes,
getBytes,
k8sScalarUnitsExcluding,
} from "../../../../common/utils";
import { modalStyleUtils } from "../../Common/FormComponents/common/styleLibrary";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import InputUnitMenu from "../../Common/FormComponents/InputUnitMenu/InputUnitMenu";
import { setModalErrorSnackMessage } from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import { BucketQuota } from "api/consoleApi";
import { api } from "api";
import { errorToHandler } from "api/errors";
interface IEnableQuotaProps {
open: boolean;
enabled: boolean;
cfg: BucketQuota | null;
selectedBucket: string;
closeModalAndRefresh: () => void;
}
const EnableQuota = ({
open,
enabled,
cfg,
selectedBucket,
closeModalAndRefresh,
}: IEnableQuotaProps) => {
const dispatch = useAppDispatch();
const [loading, setLoading] = useState<boolean>(false);
const [quotaEnabled, setQuotaEnabled] = useState<boolean>(false);
const [quotaSize, setQuotaSize] = useState<string>("1");
const [quotaUnit, setQuotaUnit] = useState<string>("Ti");
const [validInput, setValidInput] = useState<boolean>(false);
useEffect(() => {
if (enabled) {
setQuotaEnabled(true);
if (cfg) {
const unitCalc = calculateBytes(cfg.quota || 0, true, false, true);
setQuotaSize(unitCalc.total.toString());
setQuotaUnit(unitCalc.unit);
setValidInput(true);
}
}
}, [enabled, cfg]);
useEffect(() => {
const valRegExp = /^\d*(?:\.\d{1,2})?$/;
if (!quotaEnabled) {
setValidInput(true);
return;
}
setValidInput(valRegExp.test(quotaSize));
}, [quotaEnabled, quotaSize]);
const enableBucketEncryption = () => {
if (loading || !validInput) {
return;
}
api.buckets
.setBucketQuota(selectedBucket, {
enabled: quotaEnabled,
amount: parseInt(getBytes(quotaSize, quotaUnit, true)),
quota_type: "hard",
})
.then(() => {
setLoading(false);
closeModalAndRefresh();
})
.catch((err) => {
setLoading(false);
dispatch(setModalErrorSnackMessage(errorToHandler(err.error)));
});
};
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
closeModalAndRefresh();
}}
title="Enable Bucket Quota"
titleIcon={<BucketQuotaIcon />}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
enableBucketEncryption();
}}
>
<FormLayout withBorders={false} containerPadding={false}>
<Switch
value="bucket_quota"
id="bucket_quota"
name="bucket_quota"
checked={quotaEnabled}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setQuotaEnabled(event.target.checked);
}}
label={"Enabled"}
/>
{quotaEnabled && (
<InputBox
id="quota_size"
name="quota_size"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setQuotaSize(e.target.value);
if (!e.target.validity.valid) {
setValidInput(false);
} else {
setValidInput(true);
}
}}
label="Quota"
value={quotaSize}
required
min="1"
overlayObject={
<InputUnitMenu
id={"quota_unit"}
onUnitChange={(newValue) => {
setQuotaUnit(newValue);
}}
unitSelected={quotaUnit}
unitsList={k8sScalarUnitsExcluding(["Ki"])}
disabled={false}
/>
}
error={!validInput ? "Please enter a valid quota" : ""}
/>
)}
<Grid item xs={12} sx={modalStyleUtils.modalButtonBar}>
<Button
id={"cancel"}
type="button"
variant="regular"
disabled={loading}
onClick={() => {
closeModalAndRefresh();
}}
label={"Cancel"}
/>
<Button
id={"save"}
type="submit"
variant="callAction"
disabled={loading || !validInput}
label={"Save"}
/>
</Grid>
{loading && (
<Grid item xs={12}>
<ProgressBar />
</Grid>
)}
</FormLayout>
</form>
</ModalWrapper>
);
};
export default EnableQuota;

View File

@@ -1,164 +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, useState } from "react";
import { Box, Button, FormLayout, ModalBox, Switch } from "mds";
import { BucketVersioningResponse } from "api/consoleApi";
import { api } from "api";
import { errorToHandler } from "api/errors";
import { setErrorSnackMessage } from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import CSVMultiSelector from "../../Common/FormComponents/CSVMultiSelector/CSVMultiSelector";
import { modalStyleUtils } from "../../Common/FormComponents/common/styleLibrary";
interface IVersioningEventProps {
closeVersioningModalAndRefresh: (refresh: boolean) => void;
modalOpen: boolean;
selectedBucket: string;
versioningInfo: BucketVersioningResponse | undefined;
objectLockingEnabled: boolean;
}
const parseExcludedPrefixes = (
bucketVersioning: BucketVersioningResponse | undefined,
) => {
const excludedPrefixes = bucketVersioning?.excludedPrefixes;
if (excludedPrefixes) {
return excludedPrefixes.map((item) => item.prefix).join(",");
}
return "";
};
const EnableVersioningModal = ({
closeVersioningModalAndRefresh,
modalOpen,
selectedBucket,
versioningInfo = {},
objectLockingEnabled,
}: IVersioningEventProps) => {
const dispatch = useAppDispatch();
const [versioningLoading, setVersioningLoading] = useState<boolean>(false);
const [versionState, setVersionState] = useState<boolean>(
versioningInfo?.status === "Enabled",
);
const [excludeFolders, setExcludeFolders] = useState<boolean>(
!!versioningInfo?.excludeFolders,
);
const [excludedPrefixes, setExcludedPrefixes] = useState<string>(
parseExcludedPrefixes(versioningInfo),
);
const enableVersioning = () => {
if (versioningLoading) {
return;
}
setVersioningLoading(true);
api.buckets
.setBucketVersioning(selectedBucket, {
enabled: versionState,
excludeFolders: versionState ? excludeFolders : false,
excludePrefixes: versionState
? excludedPrefixes.split(",").filter((item) => item.trim() !== "")
: [],
})
.then(() => {
setVersioningLoading(false);
closeVersioningModalAndRefresh(true);
})
.catch((err) => {
setVersioningLoading(false);
dispatch(setErrorSnackMessage(errorToHandler(err.error)));
});
};
const resetForm = () => {
setExcludedPrefixes("");
setExcludeFolders(false);
setVersionState(false);
};
return (
<ModalBox
onClose={() => closeVersioningModalAndRefresh(false)}
open={modalOpen}
title={`Versioning on Bucket`}
>
<FormLayout withBorders={false} containerPadding={false}>
<Switch
id={"activateVersioning"}
label={"Versioning Status"}
checked={versionState}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setVersionState(e.target.checked);
}}
indicatorLabels={["Enabled", "Disabled"]}
/>
{versionState && !objectLockingEnabled && (
<Fragment>
<Switch
id={"excludeFolders"}
label={"Exclude Folders"}
checked={excludeFolders}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setExcludeFolders(e.target.checked);
}}
indicatorLabels={["Enabled", "Disabled"]}
/>
<CSVMultiSelector
elements={excludedPrefixes}
label={"Excluded Prefixes"}
name={"excludedPrefixes"}
onChange={(value: string | string[]) => {
let valCh = "";
if (Array.isArray(value)) {
valCh = value.join(",");
} else {
valCh = value;
}
setExcludedPrefixes(valCh);
}}
withBorder={true}
/>
</Fragment>
)}
<Box sx={modalStyleUtils.modalButtonBar}>
<Button
id={"clear"}
type="button"
variant="regular"
color="primary"
onClick={resetForm}
label={"Clear"}
/>
<Button
type="submit"
variant="callAction"
onClick={enableVersioning}
id="saveTag"
label={"Save"}
/>
</Box>
</FormLayout>
</ModalBox>
);
};
export default EnableVersioningModal;

View File

@@ -1,178 +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, { useEffect, useState, Fragment } from "react";
import { api } from "api";
import { BucketAccess } from "api/consoleApi";
import { errorToHandler } from "api/errors";
import {
Box,
Button,
ChangeAccessPolicyIcon,
FormLayout,
Grid,
Select,
} from "mds";
import { modalStyleUtils } from "../../Common/FormComponents/common/styleLibrary";
import { setModalErrorSnackMessage } from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import { emptyPolicy } from "../../Policies/utils";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import CodeMirrorWrapper from "../../Common/FormComponents/CodeMirrorWrapper/CodeMirrorWrapper";
interface ISetAccessPolicyProps {
open: boolean;
bucketName: string;
actualPolicy: BucketAccess | string;
actualDefinition: string;
closeModalAndRefresh: () => void;
}
const SetAccessPolicy = ({
open,
bucketName,
actualPolicy,
actualDefinition,
closeModalAndRefresh,
}: ISetAccessPolicyProps) => {
const dispatch = useAppDispatch();
const [addLoading, setAddLoading] = useState<boolean>(false);
const [accessPolicy, setAccessPolicy] = useState<BucketAccess | string>("");
const [policyDefinition, setPolicyDefinition] = useState<string>(emptyPolicy);
const addRecord = (event: React.FormEvent) => {
event.preventDefault();
if (addLoading || !accessPolicy) {
return;
}
setAddLoading(true);
api.buckets
.bucketSetPolicy(bucketName, {
access: accessPolicy as BucketAccess,
definition: policyDefinition,
})
.then(() => {
setAddLoading(false);
closeModalAndRefresh();
})
.catch((err) => {
setAddLoading(false);
dispatch(setModalErrorSnackMessage(errorToHandler(err.error)));
});
};
useEffect(() => {
setAccessPolicy(actualPolicy);
setPolicyDefinition(
actualDefinition
? JSON.stringify(JSON.parse(actualDefinition), null, 4)
: emptyPolicy,
);
}, [setAccessPolicy, actualPolicy, setPolicyDefinition, actualDefinition]);
return (
<ModalWrapper
title="Change Access Policy"
modalOpen={open}
onClose={() => {
closeModalAndRefresh();
}}
titleIcon={<ChangeAccessPolicyIcon />}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
addRecord(e);
}}
>
<FormLayout withBorders={false} containerPadding={false}>
<Select
value={accessPolicy}
label="Access Policy"
id="select-access-policy"
name="select-access-policy"
onChange={(value) => {
setAccessPolicy(value as BucketAccess);
}}
options={[
{ value: BucketAccess.PRIVATE, label: "Private" },
{ value: BucketAccess.PUBLIC, label: "Public" },
{ value: BucketAccess.CUSTOM, label: "Custom" },
]}
/>
{accessPolicy === "PUBLIC" && (
<Box
className={"muted"}
style={{
marginTop: "25px",
fontSize: "14px",
fontStyle: "italic",
}}
>
* Warning: With Public access anyone will be able to upload,
download and delete files from this Bucket *
</Box>
)}
{accessPolicy === "CUSTOM" && (
<Grid item xs={12}>
<CodeMirrorWrapper
label={`Write Policy`}
value={policyDefinition}
onChange={(value) => {
setPolicyDefinition(value);
}}
editorHeight={"300px"}
helptip={
<Fragment>
<a
target="blank"
href="https://min.io/docs/minio/kubernetes/upstream/administration/identity-access-management/policy-based-access-control.html#policy-document-structure"
>
Guide to access policy structure
</a>
</Fragment>
}
/>
</Grid>
)}
</FormLayout>
<Box sx={modalStyleUtils.modalButtonBar}>
<Button
id={"cancel"}
type="button"
variant="regular"
onClick={() => {
closeModalAndRefresh();
}}
disabled={addLoading}
label={"Cancel"}
/>
<Button
id={"set"}
type="submit"
variant="callAction"
disabled={
addLoading || (accessPolicy === "CUSTOM" && !policyDefinition)
}
label={"Set"}
/>
</Box>
</form>
</ModalWrapper>
);
};
export default SetAccessPolicy;

View File

@@ -1,222 +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, { useEffect, useState, Fragment } from "react";
import {
Button,
Loader,
Grid,
FormLayout,
RadioGroup,
InputBox,
ProgressBar,
} from "mds";
import { api } from "api";
import { ObjectRetentionMode, ObjectRetentionUnit } from "api/consoleApi";
import { errorToHandler } from "api/errors";
import { modalStyleUtils } from "../../Common/FormComponents/common/styleLibrary";
import { setModalErrorSnackMessage } from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
interface ISetRetentionConfigProps {
open: boolean;
bucketName: string;
closeModalAndRefresh: () => void;
}
const SetRetentionConfig = ({
open,
bucketName,
closeModalAndRefresh,
}: ISetRetentionConfigProps) => {
const dispatch = useAppDispatch();
const [addLoading, setAddLoading] = useState<boolean>(false);
const [loadingForm, setLoadingForm] = useState<boolean>(true);
const [retentionMode, setRetentionMode] = useState<
ObjectRetentionMode | undefined
>(ObjectRetentionMode.Compliance);
const [retentionUnit, setRetentionUnit] = useState<
ObjectRetentionUnit | undefined
>(ObjectRetentionUnit.Days);
const [retentionValidity, setRetentionValidity] = useState<
number | undefined
>(1);
const [valid, setValid] = useState<boolean>(false);
const setRetention = (event: React.FormEvent) => {
event.preventDefault();
if (addLoading) {
return;
}
setAddLoading(true);
api.buckets
.setBucketRetentionConfig(bucketName, {
mode: retentionMode || ObjectRetentionMode.Compliance,
unit: retentionUnit || ObjectRetentionUnit.Days,
validity: retentionValidity || 1,
})
.then(() => {
setAddLoading(false);
closeModalAndRefresh();
})
.catch((err) => {
setAddLoading(false);
dispatch(setModalErrorSnackMessage(errorToHandler(err.error)));
});
};
useEffect(() => {
if (Number.isNaN(retentionValidity) || (retentionValidity || 1) < 1) {
setValid(false);
return;
}
setValid(true);
}, [retentionValidity]);
useEffect(() => {
if (loadingForm) {
api.buckets
.getBucketRetentionConfig(bucketName)
.then((res) => {
setLoadingForm(false);
// We set default values
setRetentionMode(res.data.mode);
setRetentionValidity(res.data.validity);
setRetentionUnit(res.data.unit);
})
.catch(() => {
setLoadingForm(false);
});
}
}, [loadingForm, bucketName]);
return (
<ModalWrapper
title="Set Retention Configuration"
modalOpen={open}
onClose={() => {
closeModalAndRefresh();
}}
>
{loadingForm ? (
<Loader style={{ width: 16, height: 16 }} />
) : (
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
setRetention(e);
}}
>
<FormLayout containerPadding={false} withBorders={false}>
<RadioGroup
currentValue={retentionMode as string}
id="retention_mode"
name="retention_mode"
label="Retention Mode"
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
setRetentionMode(e.target.value as ObjectRetentionMode);
}}
selectorOptions={[
{ value: "compliance", label: "Compliance" },
{ value: "governance", label: "Governance" },
]}
helpTip={
<Fragment>
{" "}
<a
href="https://min.io/docs/minio/macos/administration/object-management/object-retention.html#minio-object-locking-compliance"
target="blank"
>
Compliance
</a>{" "}
lock protects Objects from write operations by all users,
including the MinIO root user.
<br />
<br />
<a
href="https://min.io/docs/minio/macos/administration/object-management/object-retention.html#minio-object-locking-governance"
target="blank"
>
Governance
</a>{" "}
lock protects Objects from write operations by non-privileged
users.
</Fragment>
}
helpTipPlacement="right"
/>
<RadioGroup
currentValue={retentionUnit as string}
id="retention_unit"
name="retention_unit"
label="Retention Unit"
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
setRetentionUnit(e.target.value as ObjectRetentionUnit);
}}
selectorOptions={[
{ value: "days", label: "Days" },
{ value: "years", label: "Years" },
]}
/>
<InputBox
type="number"
id="retention_validity"
name="retention_validity"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setRetentionValidity(e.target.valueAsNumber);
}}
label="Retention Validity"
value={String(retentionValidity)}
required
min="1"
/>
<Grid item xs={12} sx={modalStyleUtils.modalButtonBar}>
<Button
id={"cancel"}
type="button"
variant="regular"
disabled={addLoading}
onClick={() => {
closeModalAndRefresh();
}}
label={"Cancel"}
/>
<Button
id={"set"}
type="submit"
variant="callAction"
color="primary"
disabled={addLoading || !valid}
label={"Set"}
/>
</Grid>
{addLoading && (
<Grid item xs={12}>
<ProgressBar />
</Grid>
)}
</FormLayout>
</form>
)}
</ModalWrapper>
);
};
export default SetRetentionConfig;

View File

@@ -1,59 +0,0 @@
// 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 from "react";
import { HardBucketQuotaIcon, Box } from "mds";
import { niceBytes } from "../../../../../common/utils";
const BucketQuotaSize = ({ quota }: { quota: any }) => {
return (
<Box
sx={{
display: "flex",
alignItems: "center",
"& .min-icon": {
height: 37,
width: 37,
},
}}
>
<HardBucketQuotaIcon />
<Box
sx={{
display: "flex",
alignItems: "flex-start",
justifyContent: "center",
flexFlow: "column",
marginLeft: "20px",
fontSize: "19px",
}}
>
<label
style={{
fontWeight: 600,
textTransform: "capitalize",
}}
>
{quota?.type} Quota
</label>
<label> {niceBytes(`${quota?.quota}`, true)}</label>
</Box>
</Box>
);
};
export default BucketQuotaSize;

View File

@@ -1,196 +0,0 @@
// 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, { useEffect, useState } from "react";
import get from "lodash/get";
import { AddIcon, Box, Loader, Tag } from "mds";
import { ErrorResponseHandler } from "../../../../../common/types";
import { IAM_SCOPES } from "../../../../../common/SecureComponent/permissions";
import { SecureComponent } from "../../../../../common/SecureComponent";
import { setErrorSnackMessage } from "../../../../../systemSlice";
import { useAppDispatch } from "../../../../../store";
import useApi from "../../../Common/Hooks/useApi";
import withSuspense from "../../../Common/Components/withSuspense";
const AddBucketTagModal = withSuspense(
React.lazy(() => import("../AddBucketTagModal")),
);
const DeleteBucketTagModal = withSuspense(
React.lazy(() => import("../DeleteBucketTagModal")),
);
type BucketTagProps = {
bucketName: string;
};
interface Details {
tags: object;
}
interface Bucket {
details: Details;
name: string;
}
const BucketTags = ({ bucketName }: BucketTagProps) => {
const dispatch = useAppDispatch();
const [tags, setTags] = useState<any>(null);
const [tagModalOpen, setTagModalOpen] = useState<boolean>(false);
const [tagKeys, setTagKeys] = useState<string[]>([]);
const [selectedTag, setSelectedTag] = useState<string[]>(["", ""]);
const [deleteTagModalOpen, setDeleteTagModalOpen] = useState<boolean>(false);
const closeAddTagModal = (refresh: boolean) => {
setTagModalOpen(false);
if (refresh) {
fetchTags();
}
};
const deleteTag = (tagKey: string, tagLabel: string) => {
setSelectedTag([tagKey, tagLabel]);
setDeleteTagModalOpen(true);
};
const closeDeleteTagModal = (refresh: boolean) => {
setDeleteTagModalOpen(false);
if (refresh) {
fetchTags();
}
};
const onTagLoaded = (res: Bucket) => {
if (!!res && res?.details != null) {
if (res.details.tags) {
setTags(res?.details?.tags);
setTagKeys(Object.keys(res?.details?.tags));
return;
}
setTags([]);
setTagKeys([]);
}
};
const onTagLoadFailed = (err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
};
const [isLoading, invokeTagsApi] = useApi(onTagLoaded, onTagLoadFailed);
const fetchTags = () => {
invokeTagsApi("GET", `/api/v1/buckets/${bucketName}`);
};
useEffect(() => {
fetchTags();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [bucketName]);
return (
<Box>
{isLoading ? <Loader style={{ width: 16, height: 16 }} /> : null}
<SecureComponent
scopes={[IAM_SCOPES.S3_GET_BUCKET_TAGGING, IAM_SCOPES.S3_GET_ACTIONS]}
resource={bucketName}
>
<Box
sx={{
display: "flex",
flexFlow: "column",
marginTop: 5,
}}
>
<Box sx={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
{tagKeys &&
tagKeys.map((tagKey: any, index: any) => {
const tag = get(tags, `${tagKey}`, "");
if (tag !== "") {
return (
<SecureComponent
key={`chip-${index}`}
scopes={[
IAM_SCOPES.S3_PUT_BUCKET_TAGGING,
IAM_SCOPES.S3_PUT_ACTIONS,
]}
resource={bucketName}
matchAll
errorProps={{
deleteIcon: null,
onDelete: null,
}}
>
<Tag
label={`${tagKey} : ${tag}`}
id={`tag-${tagKey}-${tag}`}
onDelete={() => {
deleteTag(tagKey, tag);
}}
/>
</SecureComponent>
);
}
return null;
})}
<SecureComponent
scopes={[
IAM_SCOPES.S3_PUT_BUCKET_TAGGING,
IAM_SCOPES.S3_PUT_ACTIONS,
]}
resource={bucketName}
errorProps={{ disabled: true, onClick: null }}
>
<Tag
label="Add tag"
icon={<AddIcon />}
id={"create-tag"}
variant={"outlined"}
onClick={() => {
setTagModalOpen(true);
}}
sx={{ cursor: "pointer", maxWidth: 90 }}
/>
</SecureComponent>
</Box>
</Box>
</SecureComponent>
{/** Modals **/}
{tagModalOpen && (
<AddBucketTagModal
modalOpen={tagModalOpen}
currentTags={tags}
bucketName={bucketName}
onCloseAndUpdate={closeAddTagModal}
/>
)}
{deleteTagModalOpen && (
<DeleteBucketTagModal
deleteOpen={deleteTagModalOpen}
currentTags={tags}
bucketName={bucketName}
onCloseAndUpdate={closeDeleteTagModal}
selectedTag={selectedTag}
/>
)}
</Box>
);
};
export default BucketTags;

View File

@@ -1,43 +0,0 @@
// 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 from "react";
import { EditIcon, IconButton } from "mds";
type EditActionButtonProps = {
disabled?: boolean;
onClick: () => void | any;
[x: string]: any;
};
const EditActionButton = ({
disabled,
onClick,
...restProps
}: EditActionButtonProps) => {
return (
<IconButton
size={"small"}
disabled={disabled}
onClick={onClick}
{...restProps}
>
<EditIcon />
</IconButton>
);
};
export default EditActionButton;

View File

@@ -1,137 +0,0 @@
// 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 from "react";
import { ActionLink, Box, HelpTip, ValuePair } from "mds";
import { SecureComponent } from "../../../../../common/SecureComponent";
import EditActionButton from "./EditActionButton";
type EditablePropertyItemProps = {
isLoading: boolean;
resourceName: string;
iamScopes: string[];
property: any;
value: any;
onEdit: () => void;
secureCmpProps?: Record<any, any>;
disabled?: boolean;
helpTip?: any;
};
const SecureAction = ({
resourceName,
iamScopes,
secureCmpProps = {},
children,
}: {
resourceName: string;
iamScopes: string[];
children: any;
secureCmpProps?: Record<any, any>;
}) => {
return (
<SecureComponent
scopes={iamScopes}
resource={resourceName}
errorProps={{ disabled: true }}
{...secureCmpProps}
>
{children}
</SecureComponent>
);
};
const EditablePropertyItem = ({
isLoading = true,
resourceName = "",
iamScopes,
secureCmpProps = {},
property = null,
value = null,
onEdit,
disabled = false,
helpTip,
}: EditablePropertyItemProps) => {
return (
<Box
sx={{
display: "flex",
alignItems: "baseline",
justifyContent: "flex-start",
gap: 10,
}}
>
<ValuePair
label={property}
value={
helpTip ? (
<SecureAction
resourceName={resourceName}
iamScopes={iamScopes}
secureCmpProps={secureCmpProps}
>
<HelpTip placement="left" content={helpTip}>
<ActionLink
isLoading={isLoading}
onClick={onEdit}
label={value}
sx={{ fontWeight: "bold", textTransform: "capitalize" }}
disabled={disabled}
/>
</HelpTip>
</SecureAction>
) : (
<SecureAction
resourceName={resourceName}
iamScopes={iamScopes}
secureCmpProps={secureCmpProps}
>
<ActionLink
isLoading={isLoading}
onClick={onEdit}
label={value}
sx={{ fontWeight: "bold", textTransform: "capitalize" }}
disabled={disabled}
/>
</SecureAction>
)
}
/>
<SecureAction
resourceName={resourceName}
iamScopes={iamScopes}
secureCmpProps={secureCmpProps}
>
<EditActionButton
onClick={onEdit}
sx={{
background: "#f8f8f8",
marginLeft: "3px",
top: 3,
"& .min-icon": {
width: "16px",
height: "16px",
},
}}
disabled={disabled}
/>
</SecureAction>
</Box>
);
};
export default EditablePropertyItem;

View File

@@ -1,50 +0,0 @@
// 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 from "react";
import { Box } from "mds";
type LabelWithIconProps = {
icon: React.ReactNode | null;
label: React.ReactNode | null;
};
const LabelWithIcon = ({ icon = null, label = null }: LabelWithIconProps) => {
return (
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 5,
marginTop: 3,
}}
>
<Box
sx={{
height: 16,
width: 16,
display: "flex",
alignItems: "center",
}}
>
{icon}
</Box>
<Box>{label}</Box>
</Box>
);
};
export default LabelWithIcon;

View File

@@ -1,59 +0,0 @@
// 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 from "react";
import { ReportedUsageFullIcon, Box } from "mds";
import { niceBytes } from "../../../../../common/utils";
const ReportedUsage = ({ bucketSize }: { bucketSize: string }) => {
return (
<Box
sx={{
display: "flex",
alignItems: "center",
"& .min-icon": {
height: 37,
width: 37,
},
}}
>
<ReportedUsageFullIcon />
<Box
sx={{
display: "flex",
alignItems: "flex-start",
justifyContent: "center",
flexFlow: "column",
marginLeft: "20px",
fontSize: "19px",
}}
>
<label
style={{
fontWeight: 600,
}}
>
Reported Usage:
</label>
<label>{niceBytes(bucketSize)}</label>
</Box>
</Box>
);
};
export default ReportedUsage;

View File

@@ -19,40 +19,10 @@ import { Navigate, Route, Routes } from "react-router-dom";
import NotFoundPage from "../../NotFoundPage";
import LoadingComponent from "../../../common/LoadingComponent";
import { IAM_PAGES } from "../../../common/SecureComponent/permissions";
const ListBuckets = React.lazy(() => import("./ListBuckets/ListBuckets"));
const BucketDetails = React.lazy(() => import("./BucketDetails/BucketDetails"));
const AddBucket = React.lazy(() => import("./ListBuckets/AddBucket/AddBucket"));
const Buckets = () => {
return (
<Routes>
<Route
path={IAM_PAGES.ADD_BUCKETS}
element={
<Suspense fallback={<LoadingComponent />}>
<AddBucket />
</Suspense>
}
/>
<Route
path="/"
element={
<Suspense fallback={<LoadingComponent />}>
<ListBuckets />
</Suspense>
}
/>
<Route
path=":bucketName/admin/*"
element={
<Suspense fallback={<LoadingComponent />}>
<BucketDetails />
</Suspense>
}
/>
<Route element={<Navigate to={`/buckets`} />} path="*" />
<Route

View File

@@ -1,704 +0,0 @@
// 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, { Fragment, useEffect, useState } from "react";
import styled from "styled-components";
import get from "lodash/get";
import { useNavigate } from "react-router-dom";
import {
BackLink,
Box,
BucketsIcon,
Button,
FormLayout,
Grid,
HelpBox,
InfoIcon,
InputBox,
PageLayout,
RadioGroup,
Switch,
SectionTitle,
ProgressBar,
} from "mds";
import { k8sScalarUnitsExcluding } from "../../../../../common/utils";
import { AppState, useAppDispatch } from "../../../../../store";
import { useSelector } from "react-redux";
import {
selDistSet,
selSiteRep,
setErrorSnackMessage,
setHelpName,
} from "../../../../../systemSlice";
import InputUnitMenu from "../../../Common/FormComponents/InputUnitMenu/InputUnitMenu";
import TooltipWrapper from "../../../Common/TooltipWrapper/TooltipWrapper";
import {
resetForm,
setEnableObjectLocking,
setExcludedPrefixes,
setExcludeFolders,
setIsDirty,
setName,
setQuota,
setQuotaSize,
setQuotaUnit,
setRetention,
setRetentionMode,
setRetentionUnit,
setRetentionValidity,
setVersioning,
} from "./addBucketsSlice";
import { addBucketAsync } from "./addBucketThunks";
import AddBucketName from "./AddBucketName";
import {
IAM_SCOPES,
permissionTooltipHelper,
} from "../../../../../common/SecureComponent/permissions";
import { hasPermission } from "../../../../../common/SecureComponent";
import BucketNamingRules from "./BucketNamingRules";
import PageHeaderWrapper from "../../../Common/PageHeaderWrapper/PageHeaderWrapper";
import { api } from "../../../../../api";
import { ObjectRetentionMode } from "../../../../../api/consoleApi";
import { errorToHandler } from "../../../../../api/errors";
import HelpMenu from "../../../HelpMenu";
import CSVMultiSelector from "../../../Common/FormComponents/CSVMultiSelector/CSVMultiSelector";
const ErrorBox = styled.div(({ theme }) => ({
color: get(theme, "signalColors.danger", "#C51B3F"),
border: `1px solid ${get(theme, "signalColors.danger", "#C51B3F")}`,
padding: 8,
borderRadius: 3,
}));
const AddBucket = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const validBucketCharacters = new RegExp(
`^[a-z0-9][a-z0-9\\.\\-]{1,61}[a-z0-9]$`,
);
const ipAddressFormat = new RegExp(`^(\\d+\\.){3}\\d+$`);
const bucketName = useSelector((state: AppState) => state.addBucket.name);
const isDirty = useSelector((state: AppState) => state.addBucket.isDirty);
const [validationResult, setValidationResult] = useState<boolean[]>([]);
const errorList = validationResult.filter((v) => !v);
const hasErrors = errorList.length > 0;
const [records, setRecords] = useState<string[]>([]);
const versioningEnabled = useSelector(
(state: AppState) => state.addBucket.versioningEnabled,
);
const excludeFolders = useSelector(
(state: AppState) => state.addBucket.excludeFolders,
);
const excludedPrefixes = useSelector(
(state: AppState) => state.addBucket.excludedPrefixes,
);
const lockingEnabled = useSelector(
(state: AppState) => state.addBucket.lockingEnabled,
);
const quotaEnabled = useSelector(
(state: AppState) => state.addBucket.quotaEnabled,
);
const quotaSize = useSelector((state: AppState) => state.addBucket.quotaSize);
const quotaUnit = useSelector((state: AppState) => state.addBucket.quotaUnit);
const retentionEnabled = useSelector(
(state: AppState) => state.addBucket.retentionEnabled,
);
const retentionMode = useSelector(
(state: AppState) => state.addBucket.retentionMode,
);
const retentionUnit = useSelector(
(state: AppState) => state.addBucket.retentionUnit,
);
const retentionValidity = useSelector(
(state: AppState) => state.addBucket.retentionValidity,
);
const addLoading = useSelector((state: AppState) => state.addBucket.loading);
const addError = useSelector((state: AppState) => state.addBucket.error);
const invalidFields = useSelector(
(state: AppState) => state.addBucket.invalidFields,
);
const lockingFieldDisabled = useSelector(
(state: AppState) => state.addBucket.lockingFieldDisabled,
);
const distributedSetup = useSelector(selDistSet);
const siteReplicationInfo = useSelector(selSiteRep);
const navigateTo = useSelector(
(state: AppState) => state.addBucket.navigateTo,
);
const lockingAllowed = hasPermission(
"*",
[
IAM_SCOPES.S3_PUT_BUCKET_VERSIONING,
IAM_SCOPES.S3_PUT_BUCKET_OBJECT_LOCK_CONFIGURATION,
IAM_SCOPES.S3_PUT_ACTIONS,
],
true,
);
const versioningAllowed = hasPermission("*", [
IAM_SCOPES.S3_PUT_BUCKET_VERSIONING,
IAM_SCOPES.S3_PUT_ACTIONS,
]);
useEffect(() => {
if (addError) {
dispatch(setErrorSnackMessage(errorToHandler(addError)));
}
}, [addError, dispatch]);
useEffect(() => {
const bucketNameErrors = [
!(isDirty && (bucketName.length < 3 || bucketName.length > 63)),
validBucketCharacters.test(bucketName),
!(
bucketName.includes(".-") ||
bucketName.includes("-.") ||
bucketName.includes("..")
),
!ipAddressFormat.test(bucketName),
!bucketName.startsWith("xn--"),
!bucketName.endsWith("-s3alias"),
!records.includes(bucketName),
];
setValidationResult(bucketNameErrors);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [bucketName, isDirty]);
useEffect(() => {
dispatch(setName(""));
dispatch(setIsDirty(false));
const fetchRecords = () => {
api.buckets
.listBuckets()
.then((res) => {
if (res.data) {
var bucketList: string[] = [];
if (res.data.buckets != null && res.data.buckets.length > 0) {
res.data.buckets.forEach((bucket) => {
bucketList.push(bucket.name);
});
}
setRecords(bucketList);
} else if (res.error) {
dispatch(setErrorSnackMessage(errorToHandler(res.error)));
}
})
.catch((err) => {
dispatch(setErrorSnackMessage(errorToHandler(err)));
});
};
fetchRecords();
}, [dispatch]);
const resForm = () => {
dispatch(resetForm());
};
useEffect(() => {
if (navigateTo !== "") {
const goTo = `${navigateTo}`;
dispatch(resetForm());
navigate(goTo);
}
}, [navigateTo, navigate, dispatch]);
useEffect(() => {
dispatch(setHelpName("add_bucket"));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Fragment>
<PageHeaderWrapper
label={
<BackLink label={"Buckets"} onClick={() => navigate("/buckets")} />
}
actions={<HelpMenu />}
/>
<PageLayout>
<FormLayout
title={"Create Bucket"}
icon={<BucketsIcon />}
helpBox={
<HelpBox
iconComponent={<BucketsIcon />}
title={"Buckets"}
help={
<Fragment>
MinIO uses buckets to organize objects. A bucket is similar to
a folder or directory in a filesystem, where each bucket can
hold an arbitrary number of objects.
<br />
<br />
<b>Versioning</b> allows to keep multiple versions of the same
object under the same key.
<br />
<br />
<b>Object Locking</b> prevents objects from being deleted.
Required to support retention and legal hold. Can only be
enabled at bucket creation.
<br />
<br />
<b>Quota</b> limits the amount of data in the bucket.
{lockingAllowed && (
<Fragment>
<br />
<br />
<b>Retention</b> imposes rules to prevent object deletion
for a period of time. Versioning must be enabled in order
to set bucket retention policies.
</Fragment>
)}
<br />
<br />
</Fragment>
}
/>
}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
dispatch(addBucketAsync());
}}
>
<Box>
<AddBucketName hasErrors={hasErrors} />
<Box sx={{ margin: "10px 0" }}>
<BucketNamingRules errorList={validationResult} />
</Box>
<SectionTitle separator>Features</SectionTitle>
<Box sx={{ marginTop: 10 }}>
{!distributedSetup && (
<Fragment>
<ErrorBox>
These features are unavailable in a single-disk setup.
<br />
Please deploy a server in{" "}
<a
href="https://min.io/docs/minio/linux/operations/install-deploy-manage/deploy-minio-multi-node-multi-drive.html?ref=con"
target="_blank"
rel="noopener"
>
Distributed Mode
</a>{" "}
to use these features.
</ErrorBox>
<br />
<br />
</Fragment>
)}
{siteReplicationInfo.enabled && (
<Fragment>
<br />
<Box
withBorders
sx={{
display: "flex",
alignItems: "center",
padding: "10px",
"& > .min-icon ": {
width: 20,
height: 20,
marginRight: 10,
},
}}
>
<InfoIcon /> Versioning setting cannot be changed as
cluster replication is enabled for this site.
</Box>
<br />
</Fragment>
)}
<Switch
value="versioned"
id="versioned"
name="versioned"
checked={versioningEnabled}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setVersioning(event.target.checked));
}}
label={"Versioning"}
disabled={
!distributedSetup ||
lockingEnabled ||
siteReplicationInfo.enabled ||
!versioningAllowed
}
tooltip={
versioningAllowed
? ""
: permissionTooltipHelper(
[
IAM_SCOPES.S3_PUT_BUCKET_VERSIONING,
IAM_SCOPES.S3_PUT_ACTIONS,
],
"Versioning",
)
}
helpTip={
<Fragment>
{lockingEnabled && versioningEnabled && (
<strong>
{" "}
You must disable Object Locking before Versioning can
be disabled <br />
</strong>
)}
MinIO supports keeping multiple{" "}
<a
href="https://min.io/docs/minio/kubernetes/upstream/administration/object-management/object-versioning.html#minio-bucket-versioning"
target="blank"
>
versions
</a>{" "}
of an object in a single bucket.
<br />
Versioning is required to enable{" "}
<a
href="https://min.io/docs/minio/macos/administration/object-management.html#object-retention"
target="blank"
>
Object Locking
</a>{" "}
and{" "}
<a
href="https://min.io/docs/minio/macos/administration/object-management/object-retention.html#object-retention-modes"
target="blank"
>
Retention
</a>
.
</Fragment>
}
helpTipPlacement="right"
/>
{versioningEnabled && distributedSetup && !lockingEnabled && (
<Fragment>
<Switch
id={"excludeFolders"}
label={"Exclude Folders"}
checked={excludeFolders}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setExcludeFolders(e.target.checked));
}}
indicatorLabels={["Enabled", "Disabled"]}
helpTip={
<Fragment>
You can choose to{" "}
<a href="https://min.io/docs/minio/windows/administration/object-management/object-versioning.html#exclude-folders-from-versioning">
exclude folders and prefixes
</a>{" "}
from versioning if Object Locking is not enabled.
<br />
MinIO requires versioning to support replication.
<br />
Objects in excluded prefixes do not replicate to any
peer site or remote site.
</Fragment>
}
helpTipPlacement="right"
/>
<CSVMultiSelector
elements={excludedPrefixes}
label={"Excluded Prefixes"}
name={"excludedPrefixes"}
onChange={(value: string | string[]) => {
let valCh = "";
if (Array.isArray(value)) {
valCh = value.join(",");
} else {
valCh = value;
}
dispatch(setExcludedPrefixes(valCh));
}}
withBorder={true}
/>
</Fragment>
)}
<Switch
value="locking"
id="locking"
name="locking"
disabled={
lockingFieldDisabled || !distributedSetup || !lockingAllowed
}
checked={lockingEnabled}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setEnableObjectLocking(event.target.checked));
if (event.target.checked && !siteReplicationInfo.enabled) {
dispatch(setVersioning(true));
}
}}
label={"Object Locking"}
tooltip={
lockingAllowed
? ``
: permissionTooltipHelper(
[
IAM_SCOPES.S3_PUT_BUCKET_VERSIONING,
IAM_SCOPES.S3_PUT_BUCKET_OBJECT_LOCK_CONFIGURATION,
IAM_SCOPES.S3_PUT_ACTIONS,
],
"Locking",
)
}
helpTip={
<Fragment>
{retentionEnabled && (
<strong>
{" "}
You must disable Retention before Object Locking can
be disabled <br />
</strong>
)}
You can only enable{" "}
<a
href="https://min.io/docs/minio/macos/administration/object-management.html#object-retention"
target="blank"
>
Object Locking
</a>{" "}
when first creating a bucket.
<br />
<br />
<a href="https://min.io/docs/minio/windows/administration/object-management/object-versioning.html#exclude-folders-from-versioning">
Exclude folders and prefixes
</a>{" "}
options will not be available if this option is enabled.
</Fragment>
}
helpTipPlacement="right"
/>
<Switch
value="bucket_quota"
id="bucket_quota"
name="bucket_quota"
checked={quotaEnabled}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setQuota(event.target.checked));
}}
label={"Quota"}
disabled={!distributedSetup}
helpTip={
<Fragment>
Setting a{" "}
<a
href="https://min.io/docs/minio/linux/reference/minio-mc/mc-quota-set.html"
target="blank"
>
quota
</a>{" "}
assigns a hard limit to a bucket beyond which MinIO does
not allow writes.
</Fragment>
}
helpTipPlacement="right"
/>
{quotaEnabled && distributedSetup && (
<Fragment>
<InputBox
type="string"
id="quota_size"
name="quota_size"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setQuotaSize(e.target.value));
}}
label="Capacity"
value={quotaSize}
required
min="1"
overlayObject={
<InputUnitMenu
id={"quota_unit"}
onUnitChange={(newValue) => {
dispatch(setQuotaUnit(newValue));
}}
unitSelected={quotaUnit}
unitsList={k8sScalarUnitsExcluding(["Ki"])}
disabled={false}
/>
}
error={
invalidFields.includes("quotaSize")
? "Please enter a valid quota"
: ""
}
/>
</Fragment>
)}
{versioningEnabled && distributedSetup && lockingAllowed && (
<Switch
value="bucket_retention"
id="bucket_retention"
name="bucket_retention"
checked={retentionEnabled}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setRetention(event.target.checked));
}}
label={"Retention"}
helpTip={
<Fragment>
MinIO supports setting both{" "}
<a
href="https://min.io/docs/minio/macos/administration/object-management/object-retention.html#configure-bucket-default-object-retention"
target="blank"
>
bucket-default
</a>{" "}
and per-object retention rules.
<br />
<br /> For per-object retention settings, defer to the
documentation for the PUT operation used by your
preferred SDK.
</Fragment>
}
helpTipPlacement="right"
/>
)}
{retentionEnabled && distributedSetup && (
<Fragment>
<RadioGroup
currentValue={retentionMode}
id="retention_mode"
name="retention_mode"
label="Mode"
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
dispatch(
setRetentionMode(
e.target.value as ObjectRetentionMode,
),
);
}}
selectorOptions={[
{ value: "compliance", label: "Compliance" },
{ value: "governance", label: "Governance" },
]}
helpTip={
<Fragment>
{" "}
<a
href="https://min.io/docs/minio/macos/administration/object-management/object-retention.html#minio-object-locking-compliance"
target="blank"
>
Compliance
</a>{" "}
lock protects Objects from write operations by all
users, including the MinIO root user.
<br />
<br />
<a
href="https://min.io/docs/minio/macos/administration/object-management/object-retention.html#minio-object-locking-governance"
target="blank"
>
Governance
</a>{" "}
lock protects Objects from write operations by
non-privileged users.
</Fragment>
}
helpTipPlacement="right"
/>
<InputBox
type="number"
id="retention_validity"
name="retention_validity"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setRetentionValidity(e.target.valueAsNumber));
}}
label="Validity"
value={String(retentionValidity)}
required
overlayObject={
<InputUnitMenu
id={"retention_unit"}
onUnitChange={(newValue) => {
dispatch(setRetentionUnit(newValue));
}}
unitSelected={retentionUnit}
unitsList={[
{ value: "days", label: "Days" },
{ value: "years", label: "Years" },
]}
disabled={false}
/>
}
/>
</Fragment>
)}
</Box>
</Box>
<Grid
item
xs={12}
sx={{
display: "flex",
justifyContent: "flex-end",
alignItems: "center",
gap: 10,
marginTop: 15,
}}
>
<Button
id={"clear"}
type="button"
variant={"regular"}
className={"clearButton"}
onClick={resForm}
label={"Clear"}
/>
<TooltipWrapper
tooltip={
invalidFields.length > 0 || !isDirty || hasErrors
? "You must apply a valid name to the bucket"
: ""
}
>
<Button
id={"create-bucket"}
type="submit"
variant="callAction"
color="primary"
disabled={
addLoading ||
invalidFields.length > 0 ||
!isDirty ||
hasErrors
}
label={"Create Bucket"}
/>
</TooltipWrapper>
</Grid>
{addLoading && (
<Grid item xs={12}>
<ProgressBar />
</Grid>
)}
</form>
</FormLayout>
</PageLayout>
</Fragment>
);
};
export default AddBucket;

View File

@@ -0,0 +1,204 @@
// This file is part of MinIO Console Server
// Copyright (c) 2025 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 { useNavigate } from "react-router-dom";
import { Box, Button, FormLayout, Grid, ProgressBar } from "mds";
import { AppState, useAppDispatch } from "../../../../../store";
import { useSelector } from "react-redux";
import { setErrorSnackMessage, setHelpName } from "../../../../../systemSlice";
import TooltipWrapper from "../../../Common/TooltipWrapper/TooltipWrapper";
import {
resetForm,
setAddBucketOpen,
setIsDirty,
setName,
} from "./addBucketsSlice";
import { addBucketAsync } from "./addBucketThunks";
import AddBucketName from "./AddBucketName";
import { api } from "../../../../../api";
import { errorToHandler } from "../../../../../api/errors";
import ModalWrapper from "../../../Common/ModalWrapper/ModalWrapper";
const AddBucketModal = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const validBucketCharacters = new RegExp(
`^[a-z0-9][a-z0-9\\.\\-]{1,61}[a-z0-9]$`,
);
const ipAddressFormat = new RegExp(`^(\\d+\\.){3}\\d+$`);
const bucketName = useSelector((state: AppState) => state.addBucket.name);
const isDirty = useSelector((state: AppState) => state.addBucket.isDirty);
const [validationResult, setValidationResult] = useState<boolean[]>([]);
const errorList = validationResult.filter((v) => !v);
const hasErrors = errorList.length > 0;
const [records, setRecords] = useState<string[]>([]);
const addLoading = useSelector((state: AppState) => state.addBucket.loading);
const addError = useSelector((state: AppState) => state.addBucket.error);
const modalOpen = useSelector(
(state: AppState) => state.addBucket.addBucketOpen,
);
const invalidFields = useSelector(
(state: AppState) => state.addBucket.invalidFields,
);
const navigateTo = useSelector(
(state: AppState) => state.addBucket.navigateTo,
);
useEffect(() => {
if (addError) {
dispatch(setErrorSnackMessage(errorToHandler(addError)));
}
}, [addError, dispatch]);
useEffect(() => {
const bucketNameErrors = [
!(isDirty && (bucketName.length < 3 || bucketName.length > 63)),
validBucketCharacters.test(bucketName),
!(
bucketName.includes(".-") ||
bucketName.includes("-.") ||
bucketName.includes("..")
),
!ipAddressFormat.test(bucketName),
!bucketName.startsWith("xn--"),
!bucketName.endsWith("-s3alias"),
!records.includes(bucketName),
];
setValidationResult(bucketNameErrors);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [bucketName, isDirty]);
useEffect(() => {
dispatch(setName(""));
dispatch(setIsDirty(false));
const fetchRecords = () => {
api.buckets
.listBuckets()
.then((res) => {
if (res.data) {
var bucketList: string[] = [];
if (res.data.buckets != null && res.data.buckets.length > 0) {
res.data.buckets.forEach((bucket) => {
bucketList.push(bucket.name);
});
}
setRecords(bucketList);
} else if (res.error) {
dispatch(setErrorSnackMessage(errorToHandler(res.error)));
}
})
.catch((err) => {
dispatch(setErrorSnackMessage(errorToHandler(err)));
});
};
fetchRecords();
}, [dispatch]);
const resForm = () => {
dispatch(resetForm());
};
useEffect(() => {
if (navigateTo !== "") {
const goTo = `${navigateTo}`;
dispatch(setAddBucketOpen(false));
dispatch(resetForm());
navigate(goTo);
}
}, [navigateTo, navigate, dispatch]);
useEffect(() => {
dispatch(setHelpName("add_bucket"));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Fragment>
<ModalWrapper
onClose={() => {
dispatch(setAddBucketOpen(false));
}}
modalOpen={modalOpen}
title={"Create Bucket"}
>
<FormLayout withBorders={false}>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
dispatch(addBucketAsync());
}}
>
<Box>
<AddBucketName hasErrors={hasErrors} />
</Box>
<Box
sx={{
display: "flex",
justifyContent: "flex-end",
alignItems: "center",
gap: 10,
marginTop: 15,
}}
>
<Button
id={"clear"}
type="button"
variant={"regular"}
className={"clearButton"}
onClick={resForm}
label={"Clear"}
/>
<TooltipWrapper
tooltip={
invalidFields.length > 0 || !isDirty || hasErrors
? "You must apply a valid name to the bucket"
: ""
}
>
<Button
id={"create-bucket"}
type="submit"
variant="callAction"
color="primary"
disabled={
addLoading ||
invalidFields.length > 0 ||
!isDirty ||
hasErrors
}
label={"Create Bucket"}
/>
</TooltipWrapper>
</Box>
{addLoading && (
<Grid item xs={12}>
<ProgressBar />
</Grid>
)}
</form>
</FormLayout>
</ModalWrapper>
</Fragment>
);
};
export default AddBucketModal;

View File

@@ -1,140 +0,0 @@
// 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, { Fragment, useState } from "react";
import { Grid, ExpandOptionsButton, ProgressBar } from "mds";
import { AppState } from "../../../../../store";
import { useSelector } from "react-redux";
import ValidRule from "./ValidRule";
import InvalidRule from "./InvalidRule";
import NARule from "./NARule";
const BucketNamingRules = ({ errorList }: { errorList: boolean[] }) => {
const lengthRuleText =
"Bucket names must be between 3 (min) and 63 (max) characters long.";
const characterRuleText =
"Bucket names can consist only of lowercase letters, numbers, dots (.), and hyphens (-).";
const periodRuleText =
"Bucket names must not contain two adjacent periods, or a period adjacent to a hyphen.";
const ipRuleText =
"Bucket names must not be formatted as an IP address (for example, 192.168.5.4).";
const prefixRuleText = "Bucket names must not start with the prefix xn--.";
const suffixRuleText =
"Bucket names must not end with the suffix -s3alias. This suffix is reserved for access point alias names.";
const uniqueRuleText = "Bucket names must be unique within a partition.";
const bucketName = useSelector((state: AppState) => state.addBucket.name);
const [showNamingRules, setShowNamingRules] = useState<boolean>(false);
const addLoading = useSelector((state: AppState) => state.addBucket.loading);
const [
lengthRule,
validCharacters,
noAdjacentPeriods,
notIPFormat,
noPrefix,
noSuffix,
uniqueName,
] = errorList;
const toggleNamingRules = () => {
setShowNamingRules(!showNamingRules);
};
return (
<Fragment>
<ExpandOptionsButton
id={"toggle-naming-rules"}
type="button"
open={showNamingRules}
label={`${showNamingRules ? "Hide" : "View"} Bucket Naming Rules`}
onClick={() => {
toggleNamingRules();
}}
/>
{showNamingRules && (
<Grid container sx={{ fontSize: 14, paddingTop: 12 }}>
<Grid item xs={6}>
{bucketName.length === 0 ? (
<NARule ruleText={lengthRuleText} />
) : lengthRule ? (
<ValidRule ruleText={lengthRuleText} />
) : (
<InvalidRule ruleText={lengthRuleText} />
)}
{bucketName.length === 0 ? (
<NARule ruleText={characterRuleText} />
) : validCharacters ? (
<ValidRule ruleText={characterRuleText} />
) : (
<InvalidRule ruleText={characterRuleText} />
)}
{bucketName.length === 0 ? (
<NARule ruleText={periodRuleText} />
) : noAdjacentPeriods ? (
<ValidRule ruleText={periodRuleText} />
) : (
<InvalidRule ruleText={periodRuleText} />
)}
{bucketName.length === 0 ? (
<NARule ruleText={ipRuleText} />
) : notIPFormat ? (
<ValidRule ruleText={ipRuleText} />
) : (
<InvalidRule ruleText={ipRuleText} />
)}
</Grid>
<Grid item xs={6}>
{bucketName.length === 0 ? (
<NARule ruleText={prefixRuleText} />
) : noPrefix ? (
<ValidRule ruleText={prefixRuleText} />
) : (
<InvalidRule ruleText={prefixRuleText} />
)}
{bucketName.length === 0 ? (
<NARule ruleText={suffixRuleText} />
) : noSuffix ? (
<ValidRule ruleText={suffixRuleText} />
) : (
<InvalidRule ruleText={suffixRuleText} />
)}
{bucketName.length === 0 ? (
<NARule ruleText={uniqueRuleText} />
) : uniqueName ? (
<ValidRule ruleText={uniqueRuleText} />
) : (
<InvalidRule ruleText={uniqueRuleText} />
)}
</Grid>
</Grid>
)}
{addLoading && (
<Grid item xs={12}>
<ProgressBar />
</Grid>
)}
</Fragment>
);
};
export default BucketNamingRules;

View File

@@ -1,55 +0,0 @@
// 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, { Fragment } from "react";
import { ConfirmDeleteIcon, Grid } from "mds";
interface IInvalidRule {
ruleText: string;
}
const InvalidRule = ({ ruleText }: IInvalidRule) => {
return (
<Fragment>
<Grid
container
sx={{
color: "#C83B51",
display: "flex",
justifyContent: "flex-start",
}}
>
<Grid item xs={1} sx={{ paddingRight: 1 }}>
<ConfirmDeleteIcon width={"16px"} height={"16px"} />
</Grid>
<Grid
item
xs={9}
sx={{
color: "#C83B51",
display: "flex",
justifyContent: "flex-start",
paddingLeft: 1,
}}
>
{ruleText}
</Grid>
</Grid>
</Fragment>
);
};
export default InvalidRule;

View File

@@ -1,52 +0,0 @@
// 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, { Fragment } from "react";
import { CircleIcon, Grid } from "mds";
interface INARule {
ruleText: string;
}
const NARule = ({ ruleText }: INARule) => {
return (
<Fragment>
<Grid container sx={{ display: "flex", justifyContent: "flex-start" }}>
<Grid item xs={1}>
<CircleIcon
width={"12px"}
height={"12px"}
style={{ color: "#8f949c" }}
/>
</Grid>
<Grid
item
xs={9}
sx={{
color: "#8f949c",
display: "flex",
justifyContent: "flex-start",
}}
style={{}}
>
{ruleText}
</Grid>
</Grid>
</Fragment>
);
};
export default NARule;

View File

@@ -1,51 +0,0 @@
// 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, { Fragment } from "react";
import { ConfirmModalIcon, Grid } from "mds";
interface IValidRule {
ruleText: string;
}
const ValidRule = ({ ruleText }: IValidRule) => {
return (
<Fragment>
<Grid container style={{ display: "flex", justifyContent: "flex-start" }}>
<Grid item xs={1}>
<ConfirmModalIcon
width={"16px"}
height={"16px"}
style={{ color: "#18BF42" }}
/>
</Grid>
<Grid
item
xs={9}
sx={{
color: "#8f949c",
display: "flex",
justifyContent: "flex-start",
}}
>
{ruleText}
</Grid>
</Grid>
</Fragment>
);
};
export default ValidRule;

View File

@@ -14,15 +14,10 @@
// 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 { getBytes } from "../../../../../common/utils";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { AppState } from "../../../../../store";
import { api } from "../../../../../api";
import {
MakeBucketRequest,
ObjectRetentionMode,
ObjectRetentionUnit,
} from "../../../../../api/consoleApi";
import { MakeBucketRequest } from "../../../../../api/consoleApi";
export const addBucketAsync = createAsyncThunk(
"buckets/addBucketAsync",
@@ -30,57 +25,11 @@ export const addBucketAsync = createAsyncThunk(
const state = getState() as AppState;
const bucketName = state.addBucket.name;
const versioningEnabled = state.addBucket.versioningEnabled;
const lockingEnabled = state.addBucket.lockingEnabled;
const quotaEnabled = state.addBucket.quotaEnabled;
const quotaSize = state.addBucket.quotaSize;
const quotaUnit = state.addBucket.quotaUnit;
const retentionEnabled = state.addBucket.retentionEnabled;
const retentionMode = state.addBucket.retentionMode;
const retentionUnit = state.addBucket.retentionUnit;
const retentionValidity = state.addBucket.retentionValidity;
const distributedSetup = state.system.distributedSetup;
const siteReplicationInfo = state.system.siteReplicationInfo;
const excludeFolders = state.addBucket.excludeFolders;
const excludedPrefixes = state.addBucket.excludedPrefixes;
let request: MakeBucketRequest = {
name: bucketName,
versioning: {
enabled:
distributedSetup && !siteReplicationInfo.enabled
? versioningEnabled
: false,
excludePrefixes:
distributedSetup && !siteReplicationInfo.enabled && !lockingEnabled
? excludedPrefixes.split(",").filter((item) => item.trim() !== "")
: [],
excludeFolders:
distributedSetup && !siteReplicationInfo.enabled && !lockingEnabled
? excludeFolders
: false,
},
locking: distributedSetup ? lockingEnabled : false,
};
if (distributedSetup) {
if (quotaEnabled) {
const amount = getBytes(quotaSize, quotaUnit, true);
request.quota = {
enabled: true,
quota_type: "hard",
amount: parseInt(amount),
};
}
if (retentionEnabled) {
request.retention = {
mode: retentionMode as ObjectRetentionMode,
unit: retentionUnit as ObjectRetentionUnit,
validity: retentionValidity,
};
}
}
try {
return await api.buckets.makeBucket(request);
} catch (err: any) {

View File

@@ -16,47 +16,25 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { addBucketAsync } from "./addBucketThunks";
import { ApiError, ObjectRetentionMode } from "api/consoleApi";
import { ApiError } from "api/consoleApi";
interface AddBucketState {
loading: boolean;
isDirty: boolean;
addBucketOpen: boolean;
invalidFields: string[];
name: string;
versioningEnabled: boolean;
lockingEnabled: boolean;
lockingFieldDisabled: boolean;
quotaEnabled: boolean;
quotaSize: string;
quotaUnit: string;
retentionEnabled: boolean;
retentionMode: ObjectRetentionMode;
retentionUnit: string;
retentionValidity: number;
navigateTo: string;
excludeFolders: boolean;
excludedPrefixes: string;
error: ApiError | null;
}
const initialState: AddBucketState = {
loading: false,
isDirty: false,
addBucketOpen: false,
invalidFields: [],
name: "",
versioningEnabled: false,
lockingEnabled: false,
lockingFieldDisabled: false,
quotaEnabled: false,
quotaSize: "1",
quotaUnit: "Ti",
retentionEnabled: false,
retentionMode: ObjectRetentionMode.Compliance,
retentionUnit: "days",
retentionValidity: 180,
navigateTo: "",
excludeFolders: false,
excludedPrefixes: "",
error: null,
};
@@ -67,6 +45,9 @@ const addBucketsSlice = createSlice({
setIsDirty: (state, action: PayloadAction<boolean>) => {
state.isDirty = action.payload;
},
setAddBucketOpen: (state, action: PayloadAction<boolean>) => {
state.addBucketOpen = action.payload;
},
setName: (state, action: PayloadAction<string>) => {
state.name = action.payload;
@@ -78,105 +59,15 @@ const addBucketsSlice = createSlice({
);
}
},
setVersioning: (state, action: PayloadAction<boolean>) => {
state.versioningEnabled = action.payload;
if (!state.versioningEnabled || !state.retentionEnabled) {
state.retentionEnabled = false;
state.retentionMode = ObjectRetentionMode.Compliance;
state.retentionUnit = "days";
state.retentionValidity = 180;
}
resetForm: (state) => {
state.name = "";
state.loading = false;
state.isDirty = false;
state.invalidFields = [];
state.name = "";
state.navigateTo = "";
state.error = null;
},
setExcludeFolders: (state, action: PayloadAction<boolean>) => {
state.excludeFolders = action.payload;
},
setExcludedPrefixes: (state, action: PayloadAction<string>) => {
state.excludedPrefixes = action.payload;
},
setEnableObjectLocking: (state, action: PayloadAction<boolean>) => {
state.lockingEnabled = action.payload;
},
setQuota: (state, action: PayloadAction<boolean>) => {
state.quotaEnabled = action.payload;
if (!action.payload) {
state.quotaSize = "1";
state.quotaUnit = "Ti";
state.invalidFields = state.invalidFields.filter(
(field) => field !== "quotaSize",
);
}
},
setQuotaSize: (state, action: PayloadAction<string>) => {
state.quotaSize = action.payload;
if (state.quotaEnabled) {
if (
state.quotaSize.trim() === "" ||
parseInt(state.quotaSize) === 0 ||
!/^\d*(?:\.\d{1,2})?$/.test(state.quotaSize)
) {
state.invalidFields = [...state.invalidFields, "quotaSize"];
} else {
state.invalidFields = state.invalidFields.filter(
(field) => field !== "quotaSize",
);
}
}
},
setQuotaUnit: (state, action: PayloadAction<string>) => {
state.quotaUnit = action.payload;
},
setRetention: (state, action: PayloadAction<boolean>) => {
state.retentionEnabled = action.payload;
if (!state.versioningEnabled || !state.retentionEnabled) {
state.retentionEnabled = false;
state.retentionMode = ObjectRetentionMode.Compliance;
state.retentionUnit = "days";
state.retentionValidity = 180;
}
if (state.retentionEnabled) {
// if retention is enabled, then object locking should be enabled as well
state.lockingEnabled = true;
state.lockingFieldDisabled = true;
} else {
state.lockingFieldDisabled = false;
}
if (
state.retentionEnabled &&
(Number.isNaN(state.retentionValidity) || state.retentionValidity < 1)
) {
state.invalidFields = [...state.invalidFields, "retentionValidity"];
} else {
state.invalidFields = state.invalidFields.filter(
(field) => field !== "retentionValidity",
);
}
},
setRetentionMode: (state, action: PayloadAction<ObjectRetentionMode>) => {
state.retentionMode = action.payload;
},
setRetentionUnit: (state, action: PayloadAction<string>) => {
state.retentionUnit = action.payload;
},
setRetentionValidity: (state, action: PayloadAction<number>) => {
state.retentionValidity = action.payload;
if (
state.retentionEnabled &&
(Number.isNaN(state.retentionValidity) || state.retentionValidity < 1)
) {
state.invalidFields = [...state.invalidFields, "retentionValidity"];
} else {
state.invalidFields = state.invalidFields.filter(
(field) => field !== "retentionValidity",
);
}
},
resetForm: (state) => initialState,
},
extraReducers: (builder) => {
builder
@@ -193,28 +84,14 @@ const addBucketsSlice = createSlice({
state.error = null;
if (action.payload) {
state.navigateTo = action.payload.data.bucketName
? "/buckets"
: `/buckets/${action.payload.data.bucketName}/admin`;
? `/browser/${action.payload.data.bucketName}`
: `/browser`;
}
});
},
});
export const {
setName,
setIsDirty,
setVersioning,
setEnableObjectLocking,
setQuota,
setQuotaSize,
setQuotaUnit,
resetForm,
setRetention,
setRetentionMode,
setRetentionUnit,
setRetentionValidity,
setExcludedPrefixes,
setExcludeFolders,
} = addBucketsSlice.actions;
export const { setName, setAddBucketOpen, setIsDirty, resetForm } =
addBucketsSlice.actions;
export default addBucketsSlice.reducer;

View File

@@ -1,254 +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, useState } from "react";
import get from "lodash/get";
import styled from "styled-components";
import { Link, useNavigate } from "react-router-dom";
import {
Box,
breakPoints,
BucketsIcon,
Checkbox,
Grid,
HelpTip,
ReportedUsageIcon,
TotalObjectsIcon,
} from "mds";
import {
calculateBytes,
niceBytes,
prettyNumber,
} from "../../../../common/utils";
import {
IAM_PERMISSIONS,
IAM_ROLES,
} from "../../../../common/SecureComponent/permissions";
import { hasPermission } from "../../../../common/SecureComponent";
import { Bucket } from "../../../../api/consoleApi";
import { usageClarifyingContent } from "screens/Console/Dashboard/BasicDashboard/ReportedUsage";
const BucketItemMain = styled.div(({ theme }) => ({
border: `${get(theme, "borderColor", "#eaeaea")} 1px solid`,
borderRadius: 3,
padding: 15,
cursor: "pointer",
"&.disabled": {
backgroundColor: get(theme, "signalColors.danger", "red"),
},
"&:hover": {
backgroundColor: get(theme, "boxBackground", "#FBFAFA"),
},
"& .bucketTitle": {
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
gap: 10,
"& h1": {
padding: 0,
margin: 0,
marginBottom: 5,
fontSize: 22,
color: get(theme, "screenTitle.iconColor", "#07193E"),
[`@media (max-width: ${breakPoints.md}px)`]: {
marginBottom: 0,
},
},
},
"& .bucketDetails": {
display: "flex",
gap: 40,
"& span": {
fontSize: 14,
},
[`@media (max-width: ${breakPoints.md}px)`]: {
flexFlow: "column-reverse",
gap: 5,
},
},
"& .bucketMetrics": {
display: "flex",
alignItems: "center",
marginTop: 20,
gap: 25,
borderTop: `${get(theme, "borderColor", "#E2E2E2")} 1px solid`,
paddingTop: 20,
"& svg.bucketIcon": {
color: get(theme, "screenTitle.iconColor", "#07193E"),
fill: get(theme, "screenTitle.iconColor", "#07193E"),
},
"& .metric": {
"& .min-icon": {
color: get(theme, "fontColor", "#000"),
width: 13,
marginRight: 5,
},
},
"& .metricLabel": {
fontSize: 14,
fontWeight: "bold",
color: get(theme, "fontColor", "#000"),
},
"& .metricText": {
fontSize: 24,
fontWeight: "bold",
},
"& .unit": {
fontSize: 12,
fontWeight: "normal",
},
[`@media (max-width: ${breakPoints.md}px)`]: {
marginTop: 8,
paddingTop: 8,
},
},
}));
interface IBucketListItem {
bucket: Bucket;
onSelect: (e: React.ChangeEvent<HTMLInputElement>) => void;
selected: boolean;
bulkSelect: boolean;
}
const BucketListItem = ({
bucket,
onSelect,
selected,
bulkSelect,
}: IBucketListItem) => {
const navigate = useNavigate();
const [clickOverride, setClickOverride] = useState<boolean>(false);
const usage = niceBytes(`${bucket.size}` || "0");
const usageScalar = usage.split(" ")[0];
const usageUnit = usage.split(" ")[1];
const quota = get(bucket, "details.quota.quota", "0");
const quotaForString = calculateBytes(quota, true, false);
const manageAllowed =
hasPermission(bucket.name, IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN]) &&
false;
const accessToStr = (bucket: Bucket): string => {
if (bucket.rw_access?.read && !bucket.rw_access?.write) {
return "R";
} else if (!bucket.rw_access?.read && bucket.rw_access?.write) {
return "W";
} else if (bucket.rw_access?.read && bucket.rw_access?.write) {
return "R/W";
}
return "";
};
const onCheckboxClick = (e: React.ChangeEvent<HTMLInputElement>) => {
onSelect(e);
};
return (
<BucketItemMain
onClick={() => {
!clickOverride && navigate(`/buckets/${bucket.name}/admin`);
}}
id={`manageBucket-${bucket.name}`}
className={`bucket-item ${manageAllowed ? "disabled" : ""}`}
>
<Box className={"bucketTitle"}>
{bulkSelect && (
<Box
onClick={(e) => {
e.stopPropagation();
}}
>
<Checkbox
checked={selected}
id={`select-${bucket.name}`}
label={""}
name={`select-${bucket.name}`}
onChange={onCheckboxClick}
value={bucket.name}
/>
</Box>
)}
<h1>
{bucket.name} {manageAllowed}
</h1>
</Box>
<Box className={"bucketDetails"}>
<span id={`created-${bucket.name}`}>
<strong>Created:</strong>{" "}
{bucket.creation_date
? new Date(bucket.creation_date).toString()
: "n/a"}
</span>
<span id={`access-${bucket.name}`}>
<strong>Access:</strong> {accessToStr(bucket)}
</span>
</Box>
<Box className={"bucketMetrics"}>
<Link to={`/buckets/${bucket.name}/admin`}>
<BucketsIcon
className={"bucketIcon"}
style={{
height: 48,
width: 48,
}}
/>
</Link>
<Grid
item
className={"metric"}
onMouseEnter={() =>
bucket.details?.versioning && setClickOverride(true)
}
onMouseLeave={() =>
bucket.details?.versioning && setClickOverride(false)
}
>
{bucket.details?.versioning && (
<HelpTip content={usageClarifyingContent} placement="top">
<ReportedUsageIcon />{" "}
</HelpTip>
)}
{!bucket.details?.versioning && <ReportedUsageIcon />}
<span className={"metricLabel"}>Usage</span>
<div className={"metricText"}>
{usageScalar}
<span className={"unit"}>{usageUnit}</span>
{quota !== "0" && (
<Fragment>
{" "}
/ {quotaForString.total}
<span className={"unit"}>{quotaForString.unit}</span>
</Fragment>
)}
</div>
</Grid>
<Grid item className={"metric"}>
<TotalObjectsIcon />
<span className={"metricLabel"}>Objects</span>
<div className={"metricText"}>
{bucket.objects ? prettyNumber(bucket.objects) : 0}
</div>
</Grid>
</Box>
</BucketItemMain>
);
};
export default BucketListItem;

View File

@@ -1,497 +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 {
Box,
CheckCircleIcon,
FormLayout,
InputBox,
ReadBox,
Select,
Switch,
Tooltip,
WarnIcon,
Wizard,
} from "mds";
import get from "lodash/get";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import { getBytes, k8sScalarUnitsExcluding } from "../../../../common/utils";
import InputUnitMenu from "../../Common/FormComponents/InputUnitMenu/InputUnitMenu";
import { setModalErrorSnackMessage } from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import { api } from "api";
import { MultiBucketResponseItem } from "api/consoleApi";
import { errorToHandler } from "api/errors";
import { SelectorTypes } from "../../../../common/types";
interface IBulkReplicationModal {
open: boolean;
closeModalAndRefresh: (clearSelection: boolean) => any;
buckets: string[];
}
const AddBulkReplicationModal = ({
open,
closeModalAndRefresh,
buckets,
}: IBulkReplicationModal) => {
const dispatch = useAppDispatch();
const [bucketsToAlter, setBucketsToAlter] = useState<string[]>([]);
const [addLoading, setAddLoading] = useState<boolean>(false);
const [externalLoading, setExternalLoading] = useState<boolean>(false);
const [accessKey, setAccessKey] = useState<string>("");
const [secretKey, setSecretKey] = useState<string>("");
const [targetURL, setTargetURL] = useState<string>("");
const [region, setRegion] = useState<string>("");
const [useTLS, setUseTLS] = useState<boolean>(true);
const [replicationMode, setReplicationMode] = useState<"async" | "sync">(
"async",
);
const [bandwidthScalar, setBandwidthScalar] = useState<string>("100");
const [bandwidthUnit, setBandwidthUnit] = useState<string>("Gi");
const [healthCheck, setHealthCheck] = useState<string>("60");
const [relationBuckets, setRelationBuckets] = useState<string[]>([]);
const [remoteBucketsOpts, setRemoteBucketOpts] = useState<string[]>([]);
const [responseItem, setResponseItem] = useState<
MultiBucketResponseItem[] | undefined
>([]);
const optionsForBucketsDrop: SelectorTypes[] = remoteBucketsOpts.map(
(remoteBucketName: string) => {
return {
label: remoteBucketName,
value: remoteBucketName,
};
},
);
useEffect(() => {
if (relationBuckets.length === 0) {
const bucketsAlter: string[] = [];
const relationBucketsAlter: string[] = [];
buckets.forEach((item: string) => {
bucketsAlter.push(item);
relationBucketsAlter.push("");
});
setRelationBuckets(relationBucketsAlter);
setBucketsToAlter(bucketsAlter);
}
}, [buckets, relationBuckets.length]);
const addRecord = () => {
setAddLoading(true);
const replicate = bucketsToAlter.map((bucketName, index) => {
return {
originBucket: bucketName,
destinationBucket: relationBuckets[index],
};
});
const endURL = `${useTLS ? "https://" : "http://"}${targetURL}`;
const hc = parseInt(healthCheck);
const remoteBucketsInfo = {
accessKey: accessKey,
secretKey: secretKey,
targetURL: endURL,
region: region,
bucketsRelation: replicate,
syncMode: replicationMode,
bandwidth:
replicationMode === "async"
? parseInt(getBytes(bandwidthScalar, bandwidthUnit, true))
: 0,
healthCheckPeriod: hc,
};
api.bucketsReplication
.setMultiBucketReplication(remoteBucketsInfo)
.then((response) => {
setAddLoading(false);
const states = response.data.replicationState;
setResponseItem(states);
const filterErrors = states?.filter(
(itm) => itm.errorString && itm.errorString !== "",
);
if (filterErrors?.length === 0) {
closeModalAndRefresh(true);
} else {
setTimeout(() => {
removeSuccessItems(states);
}, 500);
}
})
.catch((err) => {
setAddLoading(false);
dispatch(setModalErrorSnackMessage(errorToHandler(err.error)));
});
};
const retrieveRemoteBuckets = (
wizardPageJump: (page: number | string) => void,
) => {
const remoteConnectInfo = {
accessKey: accessKey,
secretKey: secretKey,
targetURL: targetURL,
useTLS,
};
setExternalLoading(true);
api.listExternalBuckets
.listExternalBuckets(remoteConnectInfo)
.then((res) => {
const buckets = get(res.data, "buckets", []);
if (buckets && buckets.length > 0) {
const arrayReplaceBuckets = buckets.map((element: any) => {
return element.name;
});
setRemoteBucketOpts(arrayReplaceBuckets);
}
wizardPageJump("++");
setExternalLoading(false);
})
.catch((err) => {
setExternalLoading(false);
dispatch(setModalErrorSnackMessage(errorToHandler(err.error)));
});
};
const stateOfItem = (initialBucket: string) => {
if (responseItem && responseItem.length > 0) {
const bucketResponse = responseItem.find(
(item) => item.originBucket === initialBucket,
);
if (bucketResponse) {
const errString = get(bucketResponse, "errorString", "");
if (errString) {
return errString;
}
return "";
}
}
return "n/a";
};
const LogoToShow = ({ errString }: { errString: string }) => {
switch (errString) {
case "":
return (
<Box
sx={{
color: "#42C91A",
}}
>
<CheckCircleIcon />
</Box>
);
case "n/a":
return null;
default:
if (errString) {
return (
<Box
sx={{
color: "#C72C48",
}}
>
<Tooltip tooltip={errString} placement="top">
<WarnIcon />
</Tooltip>
</Box>
);
}
}
return null;
};
const updateItem = (indexItem: number, value: string) => {
const updatedList = [...relationBuckets];
updatedList[indexItem] = value;
setRelationBuckets(updatedList);
};
const itemDisplayBulk = (indexItem: number) => {
if (remoteBucketsOpts.length > 0) {
return (
<Fragment>
<Select
label=""
id={`assign-bucket-${indexItem}`}
name={`assign-bucket-${indexItem}`}
value={relationBuckets[indexItem]}
onChange={(value) => {
updateItem(indexItem, value);
}}
options={optionsForBucketsDrop}
disabled={addLoading}
/>
</Fragment>
);
}
return (
<Fragment>
<InputBox
id={`assign-bucket-${indexItem}`}
name={`assign-bucket-${indexItem}`}
label=""
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
updateItem(indexItem, event.target.value);
}}
value={relationBuckets[indexItem]}
disabled={addLoading}
/>
</Fragment>
);
};
const removeSuccessItems = (
responseItem: MultiBucketResponseItem[] | undefined,
) => {
let newBucketsToAlter = [...bucketsToAlter];
let newRelationBuckets = [...relationBuckets];
responseItem?.forEach((successElement) => {
const errorString = get(successElement, "errorString", "");
if (!errorString || errorString === "") {
const indexToRemove = newBucketsToAlter.indexOf(
successElement.originBucket || "",
);
newBucketsToAlter.splice(indexToRemove, 1);
newRelationBuckets.splice(indexToRemove, 1);
}
});
setBucketsToAlter(newBucketsToAlter);
setRelationBuckets(newRelationBuckets);
};
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
closeModalAndRefresh(false);
}}
title="Set Multiple Bucket Replication"
>
<Wizard
loadingStep={addLoading || externalLoading}
wizardSteps={[
{
label: "Remote Configuration",
componentRender: (
<Fragment>
<FormLayout containerPadding={false} withBorders={false}>
<ReadBox
label="Local Buckets to replicate"
sx={{ maxWidth: "440px", width: "100%" }}
>
{bucketsToAlter.join(", ")}
</ReadBox>
<h4>Remote Endpoint Configuration</h4>
<span style={{ fontSize: 14 }}>
Please avoid the use of root credentials for this feature
<br />
<br />
</span>
<InputBox
id="accessKey"
name="accessKey"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setAccessKey(e.target.value);
}}
label="Access Key"
value={accessKey}
/>
<InputBox
id="secretKey"
name="secretKey"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setSecretKey(e.target.value);
}}
label="Secret Key"
value={secretKey}
/>
<InputBox
id="targetURL"
name="targetURL"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTargetURL(e.target.value);
}}
placeholder="play.min.io:9000"
label="Target URL"
value={targetURL}
/>
<Switch
checked={useTLS}
id="useTLS"
name="useTLS"
label="Use TLS"
onChange={(e) => {
setUseTLS(e.target.checked);
}}
value="yes"
/>
<InputBox
id="region"
name="region"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setRegion(e.target.value);
}}
label="Region"
value={region}
/>
<Select
id="replication_mode"
name="replication_mode"
onChange={(value) => {
setReplicationMode(value as "sync" | "async");
}}
label="Replication Mode"
value={replicationMode}
options={[
{ label: "Asynchronous", value: "async" },
{ label: "Synchronous", value: "sync" },
]}
/>
{replicationMode === "async" && (
<InputBox
type="number"
id="bandwidth_scalar"
name="bandwidth_scalar"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.validity.valid) {
setBandwidthScalar(e.target.value as string);
}
}}
label="Bandwidth"
value={bandwidthScalar}
min="0"
pattern={"[0-9]*"}
overlayObject={
<InputUnitMenu
id={"quota_unit"}
onUnitChange={(newValue) => {
setBandwidthUnit(newValue);
}}
unitSelected={bandwidthUnit}
unitsList={k8sScalarUnitsExcluding(["Ki"])}
disabled={false}
/>
}
/>
)}
<InputBox
id="healthCheck"
name="healthCheck"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setHealthCheck(e.target.value as string);
}}
label="Health Check Duration"
value={healthCheck}
/>
</FormLayout>
</Fragment>
),
buttons: [
{
type: "custom",
label: "Next",
enabled: !externalLoading,
action: retrieveRemoteBuckets,
},
],
},
{
label: "Bucket Assignments",
componentRender: (
<Fragment>
<h3>Remote Bucket Assignments</h3>
<span style={{ fontSize: 14 }}>
Please select / type the desired remote bucket were you want
the local data to be replicated.
</span>
<Box
sx={{
display: "grid",
gridTemplateColumns: "auto auto 45px",
alignItems: "center",
justifyContent: "stretch",
"& .hide": {
opacity: 0,
transitionDuration: "0.3s",
},
}}
>
{bucketsToAlter.map((bucketName: string, index: number) => {
const errorItem = stateOfItem(bucketName);
return (
<Fragment
key={`buckets-assignation-${index.toString()}-${bucketName}`}
>
<div className={errorItem === "" ? "hide" : ""}>
{bucketName}
</div>
<div className={errorItem === "" ? "hide" : ""}>
{itemDisplayBulk(index)}
</div>
<div className={errorItem === "" ? "hide" : ""}>
{responseItem && responseItem.length > 0 && (
<LogoToShow errString={errorItem} />
)}
</div>
</Fragment>
);
})}
</Box>
</Fragment>
),
buttons: [
{
type: "back",
label: "Back",
enabled: true,
},
{
type: "next",
label: "Create",
enabled: !addLoading,
action: addRecord,
},
],
},
]}
forModal
/>
</ModalWrapper>
);
};
export default AddBulkReplicationModal;

View File

@@ -1,73 +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 } from "react";
import { ConfirmDeleteIcon } from "mds";
import { ErrorResponseHandler } from "../../../../common/types";
import { setErrorSnackMessage } from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import useApi from "../../Common/Hooks/useApi";
import ConfirmDialog from "../../Common/ModalWrapper/ConfirmDialog";
interface IDeleteBucketProps {
closeDeleteModalAndRefresh: (refresh: boolean) => void;
deleteOpen: boolean;
selectedBucket: string;
}
const DeleteBucket = ({
closeDeleteModalAndRefresh,
deleteOpen,
selectedBucket,
}: IDeleteBucketProps) => {
const dispatch = useAppDispatch();
const onDelSuccess = () => closeDeleteModalAndRefresh(true);
const onDelError = (err: ErrorResponseHandler) =>
dispatch(setErrorSnackMessage(err));
const onClose = () => closeDeleteModalAndRefresh(false);
const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError);
if (!selectedBucket) {
return null;
}
const onConfirmDelete = () => {
invokeDeleteApi("DELETE", `/api/v1/buckets/${selectedBucket}`, {
name: selectedBucket,
});
};
return (
<ConfirmDialog
title={`Delete Bucket`}
confirmText={"Delete"}
isOpen={deleteOpen}
titleIcon={<ConfirmDeleteIcon />}
isLoading={deleteLoading}
onConfirm={onConfirmDelete}
onClose={onClose}
confirmationContent={
<Fragment>
Are you sure you want to delete bucket <b>{selectedBucket}</b>? <br />
A bucket can only be deleted if it's empty.
</Fragment>
}
/>
);
};
export default DeleteBucket;

View File

@@ -1,404 +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 { useNavigate } from "react-router-dom";
import {
AddIcon,
BucketsIcon,
Button,
HelpBox,
MultipleBucketsIcon,
PageLayout,
RefreshIcon,
SelectAllIcon,
SelectMultipleIcon,
Grid,
breakPoints,
ProgressBar,
ActionLink,
} from "mds";
import { actionsTray } from "../../Common/FormComponents/common/styleLibrary";
import { SecureComponent } from "../../../../common/SecureComponent";
import {
CONSOLE_UI_RESOURCE,
IAM_PAGES,
IAM_SCOPES,
permissionTooltipHelper,
} from "../../../../common/SecureComponent/permissions";
import { setErrorSnackMessage, setHelpName } from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import { useSelector } from "react-redux";
import { selFeatures } from "../../consoleSlice";
import PageHeaderWrapper from "../../Common/PageHeaderWrapper/PageHeaderWrapper";
import { api } from "../../../../api";
import { Bucket } from "../../../../api/consoleApi";
import { errorToHandler } from "../../../../api/errors";
import HelpMenu from "../../HelpMenu";
import AutoColorIcon from "../../Common/Components/AutoColorIcon";
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
import SearchBox from "../../Common/SearchBox";
import VirtualizedList from "../../Common/VirtualizedList/VirtualizedList";
import hasPermission from "../../../../common/SecureComponent/accessControl";
import BucketListItem from "./BucketListItem";
import BulkReplicationModal from "./BulkReplicationModal";
const ListBuckets = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const [records, setRecords] = useState<Bucket[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [filterBuckets, setFilterBuckets] = useState<string>("");
const [selectedBuckets, setSelectedBuckets] = useState<string[]>([]);
const [replicationModalOpen, setReplicationModalOpen] =
useState<boolean>(false);
const [bulkSelect, setBulkSelect] = useState<boolean>(false);
const features = useSelector(selFeatures);
const obOnly = !!features?.includes("object-browser-only");
useEffect(() => {
dispatch(setHelpName("ob_bucket_list"));
}, [dispatch]);
useEffect(() => {
if (loading) {
const fetchRecords = () => {
setLoading(true);
api.buckets.listBuckets().then((res) => {
if (res.data) {
setLoading(false);
setRecords(res.data.buckets || []);
} else if (res.error) {
setLoading(false);
dispatch(setErrorSnackMessage(errorToHandler(res.error)));
}
});
};
fetchRecords();
}
}, [loading, dispatch]);
const filteredRecords = records.filter((b: Bucket) => {
if (filterBuckets === "") {
return true;
} else {
return b.name.indexOf(filterBuckets) >= 0;
}
});
const hasBuckets = records.length > 0;
const selectListBuckets = (e: React.ChangeEvent<HTMLInputElement>) => {
const targetD = e.target;
const value = targetD.value;
const checked = targetD.checked;
let elements: string[] = [...selectedBuckets]; // We clone the selectedBuckets array
if (checked) {
// If the user has checked this field we need to push this to selectedBucketsList
elements.push(value);
} else {
// User has unchecked this field, we need to remove it from the list
elements = elements.filter((element) => element !== value);
}
setSelectedBuckets(elements);
return elements;
};
const closeBulkReplicationModal = (unselectAll: boolean) => {
setReplicationModalOpen(false);
if (unselectAll) {
setSelectedBuckets([]);
}
};
const renderItemLine = (index: number) => {
const bucket = filteredRecords[index] || null;
if (bucket) {
return (
<BucketListItem
bucket={bucket}
onSelect={selectListBuckets}
selected={selectedBuckets.includes(bucket.name)}
bulkSelect={bulkSelect}
/>
);
}
return null;
};
const selectAllBuckets = () => {
if (selectedBuckets.length === filteredRecords.length) {
setSelectedBuckets([]);
return;
}
const selectAllBuckets = filteredRecords.map((bucket) => {
return bucket.name;
});
setSelectedBuckets(selectAllBuckets);
};
const canCreateBucket = hasPermission("*", [IAM_SCOPES.S3_CREATE_BUCKET]);
const canListBuckets = hasPermission("*", [
IAM_SCOPES.S3_LIST_BUCKET,
IAM_SCOPES.S3_ALL_LIST_BUCKET,
]);
return (
<Fragment>
{replicationModalOpen && (
<BulkReplicationModal
open={replicationModalOpen}
buckets={selectedBuckets}
closeModalAndRefresh={closeBulkReplicationModal}
/>
)}
{!obOnly && (
<PageHeaderWrapper label={"Buckets"} actions={<HelpMenu />} />
)}
<PageLayout>
<Grid item xs={12} sx={actionsTray.actionsTray}>
{obOnly && (
<Grid item xs>
<AutoColorIcon marginRight={15} marginTop={10} />
</Grid>
)}
{hasBuckets && (
<SearchBox
onChange={setFilterBuckets}
placeholder="Search Buckets"
value={filterBuckets}
sx={{
minWidth: 380,
[`@media (max-width: ${breakPoints.md}px)`]: {
minWidth: 220,
},
}}
/>
)}
<Grid
item
xs={12}
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
gap: 5,
}}
>
{!obOnly && (
<Fragment>
<TooltipWrapper
tooltip={
!hasBuckets
? ""
: bulkSelect
? "Unselect Buckets"
: "Select Multiple Buckets"
}
>
<Button
id={"multiple-bucket-seection"}
onClick={() => {
setBulkSelect(!bulkSelect);
setSelectedBuckets([]);
}}
icon={<SelectMultipleIcon />}
variant={bulkSelect ? "callAction" : "regular"}
disabled={!hasBuckets}
/>
</TooltipWrapper>
{bulkSelect && (
<TooltipWrapper
tooltip={
!hasBuckets
? ""
: selectedBuckets.length === filteredRecords.length
? "Unselect All Buckets"
: "Select All Buckets"
}
>
<Button
id={"select-all-buckets"}
onClick={selectAllBuckets}
icon={<SelectAllIcon />}
variant={"regular"}
/>
</TooltipWrapper>
)}
<TooltipWrapper
tooltip={
!hasBuckets
? ""
: selectedBuckets.length === 0
? bulkSelect
? "Please select at least one bucket on which to configure Replication"
: "Use the Select Multiple Buckets button to choose buckets on which to configure Replication"
: "Set Replication"
}
>
<Button
id={"set-replication"}
onClick={() => {
setReplicationModalOpen(true);
}}
icon={<MultipleBucketsIcon />}
variant={"regular"}
disabled={selectedBuckets.length === 0}
/>
</TooltipWrapper>
</Fragment>
)}
<TooltipWrapper tooltip={"Refresh"}>
<Button
id={"refresh-buckets"}
onClick={() => {
setLoading(true);
}}
icon={<RefreshIcon />}
variant={"regular"}
/>
</TooltipWrapper>
{!obOnly && (
<TooltipWrapper
tooltip={
canCreateBucket
? ""
: permissionTooltipHelper(
[IAM_SCOPES.S3_CREATE_BUCKET],
"create a bucket",
)
}
>
<Button
id={"create-bucket"}
onClick={() => {
navigate(IAM_PAGES.ADD_BUCKETS);
}}
icon={<AddIcon />}
variant={"callAction"}
disabled={!canCreateBucket}
label={"Create Bucket"}
/>
</TooltipWrapper>
)}
</Grid>
</Grid>
{loading && <ProgressBar />}
{!loading && (
<Grid
item
xs={12}
sx={{
marginTop: 25,
height: "calc(100vh - 211px)",
"&.isEmbedded": {
height: "calc(100vh - 128px)",
},
}}
className={obOnly ? "isEmbedded" : ""}
>
{filteredRecords.length !== 0 && (
<VirtualizedList
rowRenderFunction={renderItemLine}
totalItems={filteredRecords.length}
/>
)}
{filteredRecords.length === 0 && filterBuckets !== "" && (
<Grid container>
<Grid item xs={8}>
<HelpBox
iconComponent={<BucketsIcon />}
title={"No Results"}
help={
<Fragment>
No buckets match the filtering condition
</Fragment>
}
/>
</Grid>
</Grid>
)}
{!hasBuckets && (
<Grid container>
<Grid item xs={8}>
<HelpBox
iconComponent={<BucketsIcon />}
title={"Buckets"}
help={
<Fragment>
MinIO uses buckets to organize objects. A bucket is
similar to a folder or directory in a filesystem, where
each bucket can hold an arbitrary number of objects.
<br />
{canListBuckets ? (
""
) : (
<Fragment>
<br />
{permissionTooltipHelper(
[
IAM_SCOPES.S3_LIST_BUCKET,
IAM_SCOPES.S3_ALL_LIST_BUCKET,
],
"view the buckets on this server",
)}
<br />
</Fragment>
)}
<SecureComponent
scopes={[IAM_SCOPES.S3_CREATE_BUCKET]}
resource={CONSOLE_UI_RESOURCE}
>
<br />
To get started,&nbsp;
<ActionLink
onClick={() => {
navigate(IAM_PAGES.ADD_BUCKETS);
}}
>
Create a Bucket.
</ActionLink>
</SecureComponent>
</Fragment>
}
/>
</Grid>
</Grid>
)}
</Grid>
)}
</PageLayout>
</Fragment>
);
};
export default ListBuckets;

View File

@@ -1,171 +0,0 @@
// 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 } from "react";
import {
Button,
InspectMenuIcon,
PasswordKeyIcon,
Switch,
Grid,
Box,
} from "mds";
import {
deleteCookie,
getCookieValue,
performDownload,
} from "../../../../../../common/utils";
import ModalWrapper from "../../../../Common/ModalWrapper/ModalWrapper";
import { modalStyleUtils } from "../../../../Common/FormComponents/common/styleLibrary";
import KeyRevealer from "../../../../Tools/KeyRevealer";
import { setErrorSnackMessage } from "../../../../../../systemSlice";
import { useAppDispatch } from "../../../../../../store";
interface IInspectObjectProps {
closeInspectModalAndRefresh: (refresh: boolean) => void;
inspectOpen: boolean;
inspectPath: string;
volumeName: string;
}
const InspectObject = ({
closeInspectModalAndRefresh,
inspectOpen,
inspectPath,
volumeName,
}: IInspectObjectProps) => {
const dispatch = useAppDispatch();
const onClose = () => closeInspectModalAndRefresh(false);
const [isEncrypt, setIsEncrypt] = useState<boolean>(true);
const [decryptionKey, setDecryptionKey] = useState<string>("");
const [insFileName, setInsFileName] = useState<string>("");
if (!inspectPath) {
return null;
}
const makeRequest = async (url: string) => {
return await fetch(url, { method: "GET" });
};
const performInspect = async () => {
let basename = document.baseURI.replace(window.location.origin, "");
const urlOfInspectApi = `${window.location.origin}${basename}/api/v1/admin/inspect?volume=${encodeURIComponent(volumeName)}&file=${encodeURIComponent(inspectPath + "/xl.meta")}&encrypt=${isEncrypt}`;
makeRequest(urlOfInspectApi)
.then(async (res) => {
if (!res.ok) {
const resErr: any = await res.json();
dispatch(
setErrorSnackMessage({
errorMessage: resErr.message,
detailedError: resErr.code,
}),
);
}
const blob: Blob = await res.blob();
//@ts-ignore
const filename = res.headers.get("content-disposition").split('"')[1];
const decryptKey = getCookieValue(filename) || "";
performDownload(blob, filename);
setInsFileName(filename);
if (decryptKey === "") {
onClose();
return;
}
setDecryptionKey(decryptKey);
})
.catch((err) => {
dispatch(setErrorSnackMessage(err));
});
};
const onCloseDecKeyModal = () => {
deleteCookie(insFileName);
onClose();
setDecryptionKey("");
};
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
};
return (
<React.Fragment>
{!decryptionKey && (
<ModalWrapper
modalOpen={inspectOpen}
titleIcon={<InspectMenuIcon />}
title={`Inspect Object`}
onClose={onClose}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
onSubmit(e);
}}
>
Would you like to encrypt <b>{inspectPath}</b>? <br />
<Switch
label={"Encrypt"}
indicatorLabels={["Yes", "No"]}
checked={isEncrypt}
value={"encrypt"}
id="encrypt"
name="encrypt"
onChange={(e) => {
setIsEncrypt(!isEncrypt);
}}
description=""
/>
<Grid item xs={12} sx={modalStyleUtils.modalButtonBar}>
<Button
id={"inspect"}
type="submit"
variant="callAction"
color="primary"
onClick={performInspect}
label={"Inspect"}
/>
</Grid>
</form>
</ModalWrapper>
)}
{decryptionKey ? (
<ModalWrapper
modalOpen={inspectOpen}
title="Inspect Decryption Key"
onClose={onCloseDecKeyModal}
titleIcon={<PasswordKeyIcon />}
>
<Box>
This will be displayed only once. It cannot be recovered.
<br />
Use secure medium to share this key.
</Box>
<Box>
<KeyRevealer value={decryptionKey} />
</Box>
</ModalWrapper>
) : null}
</React.Fragment>
);
};
export default InspectObject;

View File

@@ -84,14 +84,12 @@ import {
openList,
resetMessages,
resetRewind,
setAnonymousAccessOpen,
setDownloadRenameModal,
setLoadingVersions,
setNewObject,
setObjectDetailsView,
setPreviewOpen,
setReloadObjectsList,
setRetentionConfig,
setSelectedObjects,
setSelectedObjectView,
setSelectedPreview,
@@ -121,7 +119,6 @@ import RenameLongFileName from "../../../../ObjectBrowser/RenameLongFilename";
import TooltipWrapper from "../../../../Common/TooltipWrapper/TooltipWrapper";
import ListObjectsTable from "./ListObjectsTable";
import FilterObjectsSB from "../../../../ObjectBrowser/FilterObjectsSB";
import AddAccessRule from "../../../BucketDetails/AddAccessRule";
import { sanitizeFilePath } from "./utils";
const DeleteMultipleObjects = withSuspense(
@@ -187,9 +184,6 @@ const ListObjects = () => {
const versioningConfig = useSelector(
(state: AppState) => state.objectBrowser.versionInfo,
);
const lockingEnabled = useSelector(
(state: AppState) => state.objectBrowser.lockingEnabled,
);
const downloadRenameModal = useSelector(
(state: AppState) => state.objectBrowser.downloadRenameModal,
);
@@ -202,15 +196,9 @@ const ListObjects = () => {
const previewOpen = useSelector(
(state: AppState) => state.objectBrowser.previewOpen,
);
const selectedBucket = useSelector(
(state: AppState) => state.objectBrowser.selectedBucket,
);
const anonymousMode = useSelector(
(state: AppState) => state.system.anonymousMode,
);
const anonymousAccessOpen = useSelector(
(state: AppState) => state.objectBrowser.anonymousAccessOpen,
);
const records = useSelector(
(state: AppState) => state.objectBrowser?.records || [],
@@ -442,21 +430,6 @@ const ListObjects = () => {
}
}, [bucketName, loadingBucket, dispatch, anonymousMode, requestInProgress]);
// Load retention Config
useEffect(() => {
if (selectedBucket !== "") {
api.buckets
.getBucketRetentionConfig(selectedBucket)
.then((res) => {
dispatch(setRetentionConfig(res.data));
})
.catch(() => {
dispatch(setRetentionConfig(null));
});
}
}, [selectedBucket, dispatch]);
const closeDeleteMultipleModalAndRefresh = (refresh: boolean) => {
setDeleteMultipleOpen(false);
@@ -837,10 +810,6 @@ const ListObjects = () => {
dispatch(setDownloadRenameModal(null));
};
const closeAddAccessRule = () => {
dispatch(setAnonymousAccessOpen(false));
};
let createdTime = DateTime.now();
if (bucketInfo?.creation_date) {
@@ -976,14 +945,6 @@ const ListObjects = () => {
}}
/>
)}
{anonymousAccessOpen && (
<AddAccessRule
onClose={closeAddAccessRule}
bucket={bucketName}
modalOpen={anonymousAccessOpen}
prefilledRoute={`${selectedObjects[0]}*`}
/>
)}
<PageLayout variant={"full"}>
{anonymousMode && (
@@ -1271,7 +1232,6 @@ const ListObjects = () => {
bucketName={bucketName}
onClosePanel={onClosePanel}
versioningInfo={versioningConfig}
locking={lockingEnabled}
/>
)}
</DetailsListPanel>

View File

@@ -24,13 +24,10 @@ import {
DeleteIcon,
DownloadIcon,
Grid,
InspectMenuIcon,
LegalHoldIcon,
Loader,
MetadataIcon,
ObjectInfoIcon,
PreviewIcon,
RetentionIcon,
ShareIcon,
SimpleHeader,
TagsIcon,
@@ -65,11 +62,8 @@ import { displayFileIconName } from "./utils";
import PreviewFileModal from "../Preview/PreviewFileModal";
import ObjectMetaData from "../ObjectDetails/ObjectMetaData";
import ShareFile from "../ObjectDetails/ShareFile";
import SetRetention from "../ObjectDetails/SetRetention";
import DeleteObject from "../ListObjects/DeleteObject";
import SetLegalHoldModal from "../ObjectDetails/SetLegalHoldModal";
import TagsModal from "../ObjectDetails/TagsModal";
import InspectObject from "./InspectObject";
import RenameLongFileName from "../../../../ObjectBrowser/RenameLongFilename";
import TooltipWrapper from "../../../../Common/TooltipWrapper/TooltipWrapper";
@@ -89,7 +83,6 @@ interface IObjectDetailPanelProps {
internalPaths: string;
bucketName: string;
versioningInfo: BucketVersioningResponse;
locking: boolean | undefined;
onClosePanel: (hardRefresh: boolean) => void;
}
@@ -97,7 +90,6 @@ const ObjectDetailPanel = ({
internalPaths,
bucketName,
versioningInfo,
locking,
onClosePanel,
}: IObjectDetailPanelProps) => {
const dispatch = useAppDispatch();
@@ -114,10 +106,7 @@ const ObjectDetailPanel = ({
);
const [shareFileModalOpen, setShareFileModalOpen] = useState<boolean>(false);
const [retentionModalOpen, setRetentionModalOpen] = useState<boolean>(false);
const [tagModalOpen, setTagModalOpen] = useState<boolean>(false);
const [legalholdOpen, setLegalholdOpen] = useState<boolean>(false);
const [inspectModalOpen, setInspectModalOpen] = useState<boolean>(false);
const [actualInfo, setActualInfo] = useState<BucketObject | null>(null);
const [allInfoElements, setAllInfoElements] = useState<BucketObject[]>([]);
const [objectToShare, setObjectToShare] = useState<BucketObject | null>(null);
@@ -236,17 +225,6 @@ const ObjectDetailPanel = ({
tagKeys = Object.keys(actualInfo.tags);
}
const openRetentionModal = () => {
setRetentionModalOpen(true);
};
const closeRetentionModal = (updateInfo: boolean) => {
setRetentionModalOpen(false);
if (updateInfo) {
dispatch(setLoadingObjectInfo(true));
}
};
const shareObject = () => {
setShareFileModalOpen(true);
};
@@ -279,20 +257,6 @@ const ObjectDetailPanel = ({
}
};
const closeInspectModal = (reloadObjectData: boolean) => {
setInspectModalOpen(false);
if (reloadObjectData) {
dispatch(setLoadingObjectInfo(true));
}
};
const closeLegalholdModal = (reload: boolean) => {
setLegalholdOpen(false);
if (reload) {
dispatch(setLoadingObjectInfo(true));
}
};
const loaderForContainer = (
<div style={{ textAlign: "center", marginTop: 35 }}>
<Loader />
@@ -317,28 +281,10 @@ const ObjectDetailPanel = ({
currentItem,
[bucketName, actualInfo.name].join("/"),
];
const canSetLegalHold = hasPermission(bucketName, [
IAM_SCOPES.S3_PUT_OBJECT_LEGAL_HOLD,
IAM_SCOPES.S3_PUT_ACTIONS,
]);
const canSetTags = hasPermission(objectResources, [
IAM_SCOPES.S3_PUT_OBJECT_TAGGING,
IAM_SCOPES.S3_PUT_ACTIONS,
]);
const canChangeRetention = hasPermission(
objectResources,
[
IAM_SCOPES.S3_GET_OBJECT_RETENTION,
IAM_SCOPES.S3_PUT_OBJECT_RETENTION,
IAM_SCOPES.S3_GET_ACTIONS,
IAM_SCOPES.S3_PUT_ACTIONS,
],
true,
);
const canInspect = hasPermission(objectResources, [
IAM_SCOPES.ADMIN_INSPECT_DATA,
]);
const canChangeVersioning = hasPermission(objectResources, [
IAM_SCOPES.S3_GET_BUCKET_VERSIONING,
IAM_SCOPES.S3_PUT_BUCKET_VERSIONING,
@@ -402,51 +348,6 @@ const ObjectDetailPanel = ({
"preview this object",
),
},
{
action: () => {
setLegalholdOpen(true);
},
label: "Legal Hold",
disabled:
!locking ||
!distributedSetup ||
!!actualInfo.is_delete_marker ||
!canSetLegalHold ||
selectedVersion !== "",
icon: <LegalHoldIcon />,
tooltip: canSetLegalHold
? locking
? "Change Legal Hold rules for this File"
: "Object Locking must be enabled on this bucket in order to set Legal Hold"
: permissionTooltipHelper(
[IAM_SCOPES.S3_PUT_OBJECT_LEGAL_HOLD, IAM_SCOPES.S3_PUT_ACTIONS],
"change legal hold settings for this object",
),
},
{
action: openRetentionModal,
label: "Retention",
disabled:
!distributedSetup ||
!!actualInfo.is_delete_marker ||
!canChangeRetention ||
selectedVersion !== "" ||
!locking,
icon: <RetentionIcon />,
tooltip: canChangeRetention
? locking
? "Change Retention rules for this File"
: "Object Locking must be enabled on this bucket in order to set Retention Rules"
: permissionTooltipHelper(
[
IAM_SCOPES.S3_GET_OBJECT_RETENTION,
IAM_SCOPES.S3_PUT_OBJECT_RETENTION,
IAM_SCOPES.S3_GET_ACTIONS,
IAM_SCOPES.S3_PUT_ACTIONS,
],
"change Retention Rules for this object",
),
},
{
action: () => {
setTagModalOpen(true);
@@ -467,24 +368,6 @@ const ObjectDetailPanel = ({
"set Tags on this object",
),
},
{
action: () => {
setInspectModalOpen(true);
},
label: "Inspect",
disabled:
!distributedSetup ||
!!actualInfo.is_delete_marker ||
selectedVersion !== "" ||
!canInspect,
icon: <InspectMenuIcon />,
tooltip: canInspect
? "Inspect this file"
: permissionTooltipHelper(
[IAM_SCOPES.ADMIN_INSPECT_DATA],
"inspect this file",
),
},
{
action: () => {
dispatch(
@@ -538,15 +421,6 @@ const ObjectDetailPanel = ({
dataObject={objectToShare || actualInfo}
/>
)}
{retentionModalOpen && actualInfo && (
<SetRetention
open={retentionModalOpen}
closeModalAndRefresh={closeRetentionModal}
objectName={currentItem}
objectInfo={actualInfo}
bucketName={bucketName}
/>
)}
{deleteOpen && (
<DeleteObject
deleteOpen={deleteOpen}
@@ -557,15 +431,6 @@ const ObjectDetailPanel = ({
selectedVersion={selectedVersion}
/>
)}
{legalholdOpen && actualInfo && (
<SetLegalHoldModal
open={legalholdOpen}
closeModalAndRefresh={closeLegalholdModal}
objectName={actualInfo.name || ""}
bucketName={bucketName}
actualInfo={actualInfo}
/>
)}
{previewOpen && actualInfo && (
<PreviewFileModal
open={previewOpen}
@@ -584,14 +449,6 @@ const ObjectDetailPanel = ({
onCloseAndUpdate={closeAddTagModal}
/>
)}
{inspectModalOpen && actualInfo && (
<InspectObject
inspectOpen={inspectModalOpen}
volumeName={bucketName}
inspectPath={actualInfo.name || ""}
closeInspectModalAndRefresh={closeInspectModal}
/>
)}
{longFileOpen && actualInfo && (
<RenameLongFileName
open={longFileOpen}

View File

@@ -1,140 +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, { useEffect, useState } from "react";
import get from "lodash/get";
import { Box, Button, FormLayout, Grid, Switch } from "mds";
import { BucketObject, ObjectLegalHoldStatus } from "api/consoleApi";
import { api } from "api";
import { errorToHandler } from "api/errors";
import { modalStyleUtils } from "../../../../Common/FormComponents/common/styleLibrary";
import { setModalErrorSnackMessage } from "../../../../../../systemSlice";
import { useAppDispatch } from "../../../../../../store";
import ModalWrapper from "../../../../Common/ModalWrapper/ModalWrapper";
interface ISetRetentionProps {
open: boolean;
closeModalAndRefresh: (reload: boolean) => void;
objectName: string;
bucketName: string;
actualInfo: BucketObject;
}
const SetLegalHoldModal = ({
open,
closeModalAndRefresh,
objectName,
bucketName,
actualInfo,
}: ISetRetentionProps) => {
const dispatch = useAppDispatch();
const [legalHoldEnabled, setLegalHoldEnabled] = useState<boolean>(false);
const [isSaving, setIsSaving] = useState<boolean>(false);
const versionId = actualInfo.version_id;
useEffect(() => {
const status = get(actualInfo, "legal_hold_status", "OFF");
setLegalHoldEnabled(status === "ON");
}, [actualInfo]);
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
setIsSaving(true);
api.buckets
.putObjectLegalHold(
bucketName,
{
prefix: objectName,
version_id: versionId || "",
},
{
status: legalHoldEnabled
? ObjectLegalHoldStatus.Enabled
: ObjectLegalHoldStatus.Disabled,
},
)
.then(() => {
setIsSaving(false);
closeModalAndRefresh(true);
})
.catch((err) => {
dispatch(setModalErrorSnackMessage(errorToHandler(err.error)));
setIsSaving(false);
});
};
const resetForm = () => {
setLegalHoldEnabled(false);
};
return (
<ModalWrapper
title="Set Legal Hold"
modalOpen={open}
onClose={() => {
resetForm();
closeModalAndRefresh(false);
}}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
onSubmit(e);
}}
>
<FormLayout withBorders={false} containerPadding={false}>
<Box className={"inputItem"}>
<strong>Object</strong>: {bucketName + "/" + objectName}
</Box>
<Switch
value="legalhold"
id="legalhold"
name="legalhold"
checked={legalHoldEnabled}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setLegalHoldEnabled(!legalHoldEnabled);
}}
label={"Legal Hold Status"}
indicatorLabels={["Enabled", "Disabled"]}
tooltip={
"To enable this feature you need to enable versioning on the bucket before creation"
}
/>
<Grid item xs={12} sx={modalStyleUtils.modalButtonBar}>
<Button
id={"clear"}
type="button"
variant="regular"
onClick={resetForm}
label={"Clear"}
/>
<Button
id={"save"}
type="submit"
variant="callAction"
disabled={isSaving}
label={" Save"}
/>
</Grid>
</FormLayout>
</form>
</ModalWrapper>
);
};
export default SetLegalHoldModal;

View File

@@ -1,255 +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, { useEffect, useRef, useState } from "react";
import { Box, Button, FormLayout, Grid, RadioGroup, Switch } from "mds";
import { useSelector } from "react-redux";
import { BucketObject, ObjectRetentionMode } from "api/consoleApi";
import { api } from "api";
import { errorToHandler } from "api/errors";
import { modalStyleUtils } from "../../../../Common/FormComponents/common/styleLibrary";
import { twoDigitDate } from "../../../../Common/FormComponents/DateSelector/utils";
import { setModalErrorSnackMessage } from "../../../../../../systemSlice";
import { AppState, useAppDispatch } from "../../../../../../store";
import ModalWrapper from "../../../../Common/ModalWrapper/ModalWrapper";
import DateSelector from "../../../../Common/FormComponents/DateSelector/DateSelector";
interface ISetRetentionProps {
open: boolean;
closeModalAndRefresh: (updateInfo: boolean) => void;
objectName: string;
bucketName: string;
objectInfo: BucketObject;
}
interface IRefObject {
resetDate: () => void;
}
const SetRetention = ({
open,
closeModalAndRefresh,
objectName,
objectInfo,
bucketName,
}: ISetRetentionProps) => {
const dispatch = useAppDispatch();
const retentionConfig = useSelector(
(state: AppState) => state.objectBrowser.retentionConfig,
);
const [statusEnabled, setStatusEnabled] = useState<boolean>(true);
const [type, setType] = useState<ObjectRetentionMode | "">("");
const [date, setDate] = useState<string>("");
const [isDateValid, setIsDateValid] = useState<boolean>(false);
const [isSaving, setIsSaving] = useState<boolean>(false);
const [alreadyConfigured, setAlreadyConfigured] = useState<boolean>(false);
useEffect(() => {
if (objectInfo.retention_mode) {
setType(retentionConfig?.mode || ObjectRetentionMode.Governance);
setAlreadyConfigured(true);
}
// get retention_until_date if defined
if (objectInfo.retention_until_date) {
const valueDate = new Date(objectInfo.retention_until_date);
if (valueDate.toString() !== "Invalid Date") {
const year = valueDate.getFullYear();
const month = twoDigitDate(valueDate.getMonth() + 1);
const day = valueDate.getDate();
if (!isNaN(day) && month !== "NaN" && !isNaN(year)) {
setDate(`${year}-${month}-${day}`);
}
}
setAlreadyConfigured(true);
}
}, [objectInfo, retentionConfig?.mode]);
const dateElement = useRef<IRefObject>(null);
const dateFieldDisabled = () => {
return !(statusEnabled && (type === "governance" || type === "compliance"));
};
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
};
const resetForm = () => {
setStatusEnabled(false);
setType(ObjectRetentionMode.Governance);
if (dateElement.current) {
dateElement.current.resetDate();
}
};
const addRetention = (
selectedObject: string,
versionId: string | null,
expireDate: string,
) => {
api.buckets
.putObjectRetention(
bucketName,
{
prefix: selectedObject,
version_id: versionId || "",
},
{
expires: expireDate,
mode: type as ObjectRetentionMode,
},
)
.then(() => {
setIsSaving(false);
closeModalAndRefresh(true);
})
.catch((err) => {
dispatch(setModalErrorSnackMessage(errorToHandler(err.error)));
setIsSaving(false);
});
};
const disableRetention = (
selectedObject: string,
versionId: string | null,
) => {
api.buckets
.deleteObjectRetention(bucketName, {
prefix: selectedObject,
version_id: versionId || "",
})
.then(() => {
setIsSaving(false);
closeModalAndRefresh(true);
})
.catch((err) => {
dispatch(setModalErrorSnackMessage(errorToHandler(err.error)));
setIsSaving(false);
});
};
const saveNewRetentionPolicy = () => {
setIsSaving(true);
const selectedObject = objectInfo.name || "";
const versionId = objectInfo.version_id || null;
const expireDate =
!statusEnabled && type === "governance" ? "" : `${date}T23:59:59Z`;
if (!statusEnabled && type === "governance") {
disableRetention(selectedObject, versionId);
return;
}
addRetention(selectedObject, versionId, expireDate);
};
const showSwitcher =
alreadyConfigured && (type === "governance" || type === "");
return (
<ModalWrapper
title="Set Retention Policy"
modalOpen={open}
onClose={() => {
resetForm();
closeModalAndRefresh(false);
}}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
onSubmit(e);
}}
>
<FormLayout withBorders={false} containerPadding={false}>
<Box className={"inputItem"}>
<strong>Selected Object</strong>: {objectName}
</Box>
{showSwitcher && (
<Switch
value="status"
id="status"
name="status"
checked={statusEnabled}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setStatusEnabled(!statusEnabled);
}}
label={"Status"}
indicatorLabels={["Enabled", "Disabled"]}
/>
)}
<RadioGroup
currentValue={type}
id="type"
name="type"
label="Type"
disableOptions={
!statusEnabled || (alreadyConfigured && type !== "")
}
onChange={(e) => {
setType(e.target.value as ObjectRetentionMode);
}}
selectorOptions={[
{ label: "Governance", value: ObjectRetentionMode.Governance },
{ label: "Compliance", value: ObjectRetentionMode.Compliance },
]}
/>
<DateSelector
id="date"
label="Date"
disableOptions={dateFieldDisabled()}
ref={dateElement}
value={date}
borderBottom={true}
onDateChange={(date: string, isValid: boolean) => {
setIsDateValid(isValid);
if (isValid) {
setDate(date);
}
}}
/>
<Grid item xs={12} sx={modalStyleUtils.modalButtonBar}>
<Button
id={"reset"}
type="button"
variant="regular"
onClick={resetForm}
label={"Reset"}
/>
<Button
id={"save"}
type="submit"
variant="callAction"
disabled={
(statusEnabled && type === "") ||
(statusEnabled && !isDateValid) ||
isSaving
}
onClick={saveNewRetentionPolicy}
label={"Save"}
/>
</Grid>
</FormLayout>
</form>
</ModalWrapper>
);
};
export default SetRetention;

View File

@@ -1,87 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2023 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { DisabledIcon, EnabledIcon, Box } from "mds";
import { BucketVersioningResponse } from "api/consoleApi";
import LabelWithIcon from "./BucketDetails/SummaryItems/LabelWithIcon";
const VersioningInfo = ({
versioningState = {},
}: {
versioningState?: BucketVersioningResponse;
}) => {
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: 2,
}}
>
<Box sx={{ fontWeight: "medium", display: "flex", gap: 2 }}>
{versioningState.excludeFolders ? (
<LabelWithIcon
icon={
versioningState.excludeFolders ? (
<EnabledIcon style={{ color: "green" }} />
) : (
<DisabledIcon />
)
}
label={
<label style={{ textDecoration: "normal" }}>
Exclude Folders
</label>
}
/>
) : null}
</Box>
{versioningState.excludedPrefixes?.length ? (
<Box
sx={{
fontWeight: "medium",
display: "flex",
justifyItems: "end",
placeItems: "flex-start",
flexDirection: "column",
gap: 1,
}}
>
<Box>Excluded Prefixes :</Box>
<div
style={{
maxHeight: "200px",
overflowY: "auto",
placeItems: "flex-start",
justifyItems: "end",
flexDirection: "column",
display: "flex",
}}
>
{versioningState.excludedPrefixes?.map((it) => (
<div>
<strong>{it.prefix}</strong>
</div>
))}
</div>
</Box>
) : null}
</Box>
);
};
export default VersioningInfo;

View File

@@ -1,38 +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/>.
export interface BucketReplicationDestination {
bucket: string;
}
export interface BucketReplicationRule {
id: string;
status: string;
priority: number;
delete_marker_replication: boolean;
deletes_replication: boolean;
metadata_replication: boolean;
prefix?: string;
tags?: string;
destination: BucketReplicationDestination;
syncMode: string;
storageClass?: string;
existingObjects?: boolean;
}
export interface BucketReplication {
rules: BucketReplicationRule[];
}

View File

@@ -33,8 +33,6 @@ import { Action } from "kbar/lib/types";
import { routesAsKbarActions } from "./kbar-actions";
import { Box, MenuExpandedIcon } from "mds";
import { useSelector } from "react-redux";
import { selFeatures } from "./consoleSlice";
import { Bucket } from "../../api/consoleApi";
import { api } from "../../api";
@@ -109,7 +107,6 @@ const KBarStateChangeMonitor = ({
};
const CommandBar = () => {
const features = useSelector(selFeatures);
const navigate = useNavigate();
const [buckets, setBuckets] = useState<Bucket[]>([]);
@@ -127,13 +124,9 @@ const CommandBar = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const initialActions: Action[] = routesAsKbarActions(
buckets,
navigate,
features,
);
const initialActions: Action[] = routesAsKbarActions(buckets, navigate);
useRegisterActions(initialActions, [buckets, features]);
useRegisterActions(initialActions, [buckets]);
//fetch buckets everytime the kbar is shown so that new buckets created elsewhere , within first page is also shown

View File

@@ -1,82 +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 { Button, ConfirmDeleteIcon, PageLayout, SectionTitle, Grid } from "mds";
import ConfirmDialog from "./ModalWrapper/ConfirmDialog";
import PageHeaderWrapper from "./PageHeaderWrapper/PageHeaderWrapper";
import HelpMenu from "../HelpMenu";
import { setHelpName } from "../../../systemSlice";
import { useAppDispatch } from "../../../store";
const ComponentsScreen = () => {
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(setHelpName("components"));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Fragment>
<PageHeaderWrapper label={"Components"} actions={<HelpMenu />} />
<PageLayout>
<Grid container>
<Grid item xs={12}>
<SectionTitle>Confirm Dialogs</SectionTitle>
</Grid>
<Grid item xs={12}>
<p>Used to confirm a non-idempotent action.</p>
</Grid>
<Grid item xs={12}>
<Button
id={"open-dialog-test"}
type="button"
variant={"regular"}
onClick={() => {
setDialogOpen(true);
}}
label={"Open Dialog"}
/>
<ConfirmDialog
title={`Delete Bucket`}
confirmText={"Delete"}
isOpen={dialogOpen}
titleIcon={<ConfirmDeleteIcon />}
isLoading={false}
onConfirm={() => {
setDialogOpen(false);
}}
onClose={() => {
setDialogOpen(false);
}}
confirmationContent={
<Fragment>
Are you sure you want to delete bucket <b>bucket</b>
? <br />A bucket can only be deleted if it's empty.
</Fragment>
}
/>
</Grid>
</Grid>
</PageLayout>
</Fragment>
);
};
export default ComponentsScreen;

View File

@@ -1,60 +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 { Button, CopyIcon, InputLabel, ReadBox, Box } from "mds";
import CopyToClipboard from "react-copy-to-clipboard";
import { setModalSnackMessage } from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
interface ICredentialsItem {
label?: string;
value?: string;
}
const CredentialItem = ({ label = "", value = "" }: ICredentialsItem) => {
const dispatch = useAppDispatch();
return (
<Box sx={{ marginTop: 12 }}>
<InputLabel>{label}</InputLabel>
<ReadBox
actionButton={
<CopyToClipboard text={value}>
<Button
id={"copy-path"}
variant="regular"
onClick={() => {
dispatch(setModalSnackMessage(`${label} copied to clipboard`));
}}
style={{
marginRight: "5px",
width: "28px",
height: "28px",
padding: "0px",
}}
icon={<CopyIcon />}
/>
</CopyToClipboard>
}
>
{value}
</ReadBox>
</Box>
);
};
export default CredentialItem;

View File

@@ -1,271 +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 } from "react";
import get from "lodash/get";
import styled from "styled-components";
import {
Box,
Button,
DownloadIcon,
ServiceAccountCredentialsIcon,
WarnIcon,
Grid,
} from "mds";
import { NewServiceAccount } from "./types";
import ModalWrapper from "../ModalWrapper/ModalWrapper";
import CredentialItem from "./CredentialItem";
import TooltipWrapper from "../TooltipWrapper/TooltipWrapper";
import { modalStyleUtils } from "../FormComponents/common/styleLibrary";
const WarningBlock = styled.div(({ theme }) => ({
color: get(theme, "signalColors.danger", "#C51B3F"),
fontSize: ".85rem",
margin: ".5rem 0 .5rem 0",
display: "flex",
alignItems: "center",
"& svg ": {
marginRight: ".3rem",
height: 16,
width: 16,
},
}));
interface ICredentialsPromptProps {
newServiceAccount: NewServiceAccount | null;
open: boolean;
entity: string;
closeModal: () => void;
}
const download = (filename: string, text: string) => {
let element = document.createElement("a");
element.setAttribute("href", "data:text/plain;charset=utf-8," + text);
element.setAttribute("download", filename);
element.style.display = "none";
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
};
const CredentialsPrompt = ({
newServiceAccount,
open,
closeModal,
entity,
}: ICredentialsPromptProps) => {
if (!newServiceAccount) {
return null;
}
const consoleCreds = get(newServiceAccount, "console", null);
const idp = get(newServiceAccount, "idp", false);
const downloadImport = () => {
let consoleExtras = {};
if (consoleCreds) {
if (!Array.isArray(consoleCreds)) {
consoleExtras = {
url: consoleCreds.url,
accessKey: consoleCreds.accessKey,
secretKey: consoleCreds.secretKey,
api: "s3v4",
path: "auto",
};
} else {
const cCreds = consoleCreds.map((itemMap) => {
return {
url: itemMap.url,
accessKey: itemMap.accessKey,
secretKey: itemMap.secretKey,
api: "s3v4",
path: "auto",
};
});
consoleExtras = cCreds[0];
}
} else {
consoleExtras = {
url: newServiceAccount.url,
accessKey: newServiceAccount.accessKey,
secretKey: newServiceAccount.secretKey,
api: "s3v4",
path: "auto",
};
}
download(
"credentials.json",
JSON.stringify({
...consoleExtras,
}),
);
};
const downloaddAllCredentials = () => {
let allCredentials = {};
if (
consoleCreds &&
Array.isArray(consoleCreds) &&
consoleCreds.length > 1
) {
const cCreds = consoleCreds.map((itemMap) => {
return {
accessKey: itemMap.accessKey,
secretKey: itemMap.secretKey,
};
});
allCredentials = cCreds;
}
download(
"all_credentials.json",
JSON.stringify({
...allCredentials,
}),
);
};
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
closeModal();
}}
title={`New ${entity} Created`}
titleIcon={<ServiceAccountCredentialsIcon />}
>
<Grid container>
<Grid item xs={12}>
A new {entity} has been created with the following details:
{!idp && consoleCreds && (
<Fragment>
<Grid
item
xs={12}
sx={{
overflowY: "auto",
maxHeight: 350,
}}
>
<Box
sx={{
padding: ".8rem 0 0 0",
fontWeight: 600,
fontSize: ".9rem",
}}
>
Console Credentials
</Box>
{Array.isArray(consoleCreds) &&
consoleCreds.map((credentialsPair, index) => {
return (
<Fragment>
<CredentialItem
label="Access Key"
value={credentialsPair.accessKey}
/>
<CredentialItem
label="Secret Key"
value={credentialsPair.secretKey}
/>
</Fragment>
);
})}
{!Array.isArray(consoleCreds) && (
<Fragment>
<CredentialItem
label="Access Key"
value={consoleCreds.accessKey}
/>
<CredentialItem
label="Secret Key"
value={consoleCreds.secretKey}
/>
</Fragment>
)}
</Grid>
</Fragment>
)}
{(consoleCreds === null || consoleCreds === undefined) && (
<>
<CredentialItem
label="Access Key"
value={newServiceAccount.accessKey || ""}
/>
<CredentialItem
label="Secret Key"
value={newServiceAccount.secretKey || ""}
/>
</>
)}
{idp ? (
<WarningBlock>
Please Login via the configured external identity provider.
</WarningBlock>
) : (
<WarningBlock>
<WarnIcon />
<span>
Write these down, as this is the only time the secret will be
displayed.
</span>
</WarningBlock>
)}
</Grid>
<Grid item xs={12} sx={{ ...modalStyleUtils.modalButtonBar }}>
{!idp && (
<Fragment>
<TooltipWrapper
tooltip={
"Download credentials in a JSON file formatted for import using mc alias import. This will only include the default login credentials."
}
>
<Button
id={"download-button"}
label={"Download for import"}
onClick={downloadImport}
icon={<DownloadIcon />}
variant="callAction"
/>
</TooltipWrapper>
{Array.isArray(consoleCreds) && consoleCreds.length > 1 && (
<TooltipWrapper
tooltip={
"Download all access credentials to a JSON file. NOTE: This file is not formatted for import using mc alias import. If you plan to import this alias from the file, please use the Download for Import button. "
}
>
<Button
id={"download-all-button"}
label={"Download all access credentials"}
onClick={downloaddAllCredentials}
icon={<DownloadIcon />}
variant="callAction"
color="primary"
/>
</TooltipWrapper>
)}
</Fragment>
)}
</Grid>
</Grid>
</ModalWrapper>
);
};
export default CredentialsPrompt;

View File

@@ -1,29 +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/>.
export interface NewServiceAccount {
idp?: boolean;
console?: ConsoleSA | ConsoleSA[];
accessKey?: string;
secretKey?: string;
url?: string;
}
interface ConsoleSA {
accessKey: string;
secretKey: string;
url: string;
}

View File

@@ -1,184 +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, {
ChangeEvent,
createRef,
useCallback,
useEffect,
useRef,
useState,
Fragment,
} from "react";
import get from "lodash/get";
import { AddIcon, Box, HelpIcon, InputBox, InputLabel, Tooltip } from "mds";
interface ICSVMultiSelector {
elements: string;
name: string;
label: string;
tooltip?: string;
commonPlaceholder?: string;
withBorder?: boolean;
onChange: (elements: string) => void;
}
const CSVMultiSelector = ({
elements,
name,
label,
tooltip = "",
commonPlaceholder = "",
onChange,
withBorder = false,
}: ICSVMultiSelector) => {
const [currentElements, setCurrentElements] = useState<string[]>([""]);
const bottomList = createRef<HTMLDivElement>();
// Use effect to get the initial values from props
useEffect(() => {
if (
currentElements.length === 1 &&
currentElements[0] === "" &&
elements &&
elements !== ""
) {
const elementsSplit = elements.split(",");
elementsSplit.push("");
setCurrentElements(elementsSplit);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [elements, currentElements]);
// Use effect to send new values to onChange
useEffect(() => {
if (currentElements.length > 1) {
const refScroll = bottomList.current;
if (refScroll) {
refScroll.scrollIntoView(false);
}
}
}, [currentElements, bottomList]);
const onChangeCallback = useCallback(
(newString: string) => {
onChange(newString);
},
[onChange],
);
// We avoid multiple re-renders / hang issue typing too fast
const firstUpdate = useRef(true);
useEffect(() => {
if (firstUpdate.current) {
firstUpdate.current = false;
return;
}
const elementsString = currentElements
.filter((element) => element.trim() !== "")
.join(",");
onChangeCallback(elementsString);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentElements]);
// If the last input is not empty, we add a new one
const addEmptyLine = (elementsUp: string[]) => {
if (elementsUp[elementsUp.length - 1].trim() !== "") {
const cpList = [...elementsUp];
cpList.push("");
setCurrentElements(cpList);
}
};
// Onchange function for input box, we get the dataset-index & only update that value in the array
const onChangeElement = (e: ChangeEvent<HTMLInputElement>) => {
e.persist();
let updatedElement = [...currentElements];
const index = get(e.target, "dataset.index", "0");
const indexNum = parseInt(index);
updatedElement[indexNum] = e.target.value;
setCurrentElements(updatedElement);
};
const inputs = currentElements.map((element, index) => {
return (
<InputBox
key={`csv-multi-${name}-${index.toString()}`}
id={`${name}-${index.toString()}`}
label={""}
name={`${name}-${index.toString()}`}
value={currentElements[index]}
onChange={onChangeElement}
index={index}
placeholder={commonPlaceholder}
overlayIcon={index === currentElements.length - 1 ? <AddIcon /> : null}
overlayAction={() => {
addEmptyLine(currentElements);
}}
/>
);
});
return (
<Fragment>
<Box sx={{ display: "flex" }} className={"inputItem"}>
<InputLabel
sx={{
alignItems: "flex-start",
}}
>
<span>{label}</span>
{tooltip !== "" && (
<Box
sx={{
marginLeft: 5,
display: "flex",
alignItems: "center",
"& .min-icon": {
width: 13,
},
}}
>
<Tooltip tooltip={tooltip} placement="top">
<Box className={tooltip}>
<HelpIcon />
</Box>
</Tooltip>
</Box>
)}
</InputLabel>
<Box
withBorders={withBorder}
sx={{
width: "100%",
overflowY: "auto",
height: 150,
position: "relative",
}}
>
{inputs}
<div ref={bottomList} />
</Box>
</Box>
</Fragment>
);
};
export default CSVMultiSelector;

View File

@@ -1,70 +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 } from "react";
import { Button, CodeEditor, CopyIcon } from "mds";
import CopyToClipboard from "react-copy-to-clipboard";
import TooltipWrapper from "../../TooltipWrapper/TooltipWrapper";
interface ICodeWrapper {
value: string;
label?: string;
mode?: string;
tooltip?: string;
onChange: (value: string) => any;
editorHeight?: string | number;
helptip?: any;
}
const CodeMirrorWrapper = ({
value,
label = "",
tooltip = "",
mode = "json",
onChange,
editorHeight = 250,
helptip,
}: ICodeWrapper) => {
return (
<CodeEditor
value={value}
onChange={(value) => onChange(value)}
mode={mode}
tooltip={tooltip}
editorHeight={editorHeight}
label={label}
helpTools={
<Fragment>
<TooltipWrapper tooltip={"Copy to Clipboard"}>
<CopyToClipboard text={value}>
<Button
type={"button"}
id={"copy-code-mirror"}
icon={<CopyIcon />}
color={"primary"}
variant={"regular"}
/>
</CopyToClipboard>
</TooltipWrapper>
</Fragment>
}
helpTip={helptip}
helpTipPlacement="right"
/>
);
};
export default CodeMirrorWrapper;

View File

@@ -1,175 +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 } from "react";
import {
Button,
SyncIcon,
Grid,
Box,
breakPoints,
TimeIcon,
DateTimeInput,
} from "mds";
import { DateTime } from "luxon";
interface IDateRangeSelector {
timeStart: DateTime | null;
setTimeStart: (value: DateTime | null) => void;
timeEnd: DateTime | null;
setTimeEnd: (value: DateTime | null) => void;
triggerSync?: () => void;
label?: string;
startLabel?: string;
endLabel?: string;
}
const DateRangeSelector = ({
timeStart,
setTimeStart,
timeEnd,
setTimeEnd,
triggerSync,
label = "Filter:",
startLabel = "Start Time:",
endLabel = "End Time:",
}: IDateRangeSelector) => {
return (
<Grid
item
xs={12}
sx={{
"& .filter-date-input-label, .end-time-input-label": {
display: "none",
},
"& .MuiInputBase-adornedEnd.filter-date-date-time-input": {
width: "100%",
border: "1px solid #eaeaea",
paddingLeft: "8px",
paddingRight: "8px",
borderRadius: "1px",
},
"& .MuiInputAdornment-root button": {
height: "20px",
width: "20px",
marginRight: "5px",
},
"& .filter-date-input-wrapper": {
height: "30px",
width: "100%",
"& .MuiTextField-root": {
height: "30px",
width: "90%",
"& input.Mui-disabled": {
color: "#000000",
WebkitTextFillColor: "#101010",
},
},
},
}}
>
<Box
sx={{
display: "grid",
height: 40,
alignItems: "center",
gridTemplateColumns: "auto 2fr auto",
padding: 0,
[`@media (max-width: ${breakPoints.sm}px)`]: {
padding: 5,
},
[`@media (max-width: ${breakPoints.md}px)`]: {
gridTemplateColumns: "1fr",
height: "auto",
},
gap: "5px",
}}
>
<Box
sx={{ fontSize: "14px", fontWeight: 500, marginRight: "5px" }}
className={"muted"}
>
{label}
</Box>
<Box
customBorderPadding={"0px"}
sx={{
display: "grid",
height: 40,
alignItems: "center",
gridTemplateColumns: "1fr 1fr",
gap: "8px",
paddingLeft: "8px",
paddingRight: "8px",
[`@media (max-width: ${breakPoints.md}px)`]: {
height: "auto",
gridTemplateColumns: "1fr",
},
}}
>
<DateTimeInput
value={timeStart}
onChange={setTimeStart}
id="stTime"
secondsSelector={false}
pickerStartComponent={
<Fragment>
<TimeIcon />
<span>{startLabel}</span>
</Fragment>
}
/>
<DateTimeInput
value={timeEnd}
onChange={setTimeEnd}
id="endTime"
secondsSelector={false}
pickerStartComponent={
<Fragment>
<TimeIcon />
<span>{endLabel}</span>
</Fragment>
}
/>
</Box>
{triggerSync && (
<Box
sx={{
alignItems: "flex-end",
display: "flex",
justifyContent: "flex-end",
}}
>
<Button
id={"sync"}
type="button"
variant="callAction"
onClick={triggerSync}
icon={<SyncIcon />}
label={"Sync"}
/>
</Box>
)}
</Box>
</Grid>
);
};
export default DateRangeSelector;

View File

@@ -1,174 +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, {
forwardRef,
useEffect,
useImperativeHandle,
useState,
} from "react";
import { Box, HelpIcon, InputLabel, Select, Tooltip } from "mds";
import { days, months, validDate, years } from "./utils";
interface IDateSelectorProps {
id: string;
label: string;
disableOptions?: boolean;
tooltip?: string;
borderBottom?: boolean;
value?: string;
onDateChange: (date: string, isValid: boolean) => any;
}
const DateSelector = forwardRef(
(
{
id,
label,
disableOptions = false,
tooltip = "",
borderBottom = false,
onDateChange,
value = "",
}: IDateSelectorProps,
ref: any,
) => {
useImperativeHandle(ref, () => ({ resetDate }));
const [month, setMonth] = useState<string>("");
const [day, setDay] = useState<string>("");
const [year, setYear] = useState<string>("");
useEffect(() => {
// verify if there is a current value
// assume is in the format "2021-12-30"
if (value !== "") {
const valueSplit = value.split("-");
setYear(valueSplit[0]);
setMonth(valueSplit[1]);
// Turn to single digit to be displayed on dropdown buttons
setDay(`${parseInt(valueSplit[2])}`);
}
}, [value]);
useEffect(() => {
const [isValid, dateString] = validDate(year, month, day);
onDateChange(dateString, isValid);
}, [month, day, year, onDateChange]);
const resetDate = () => {
setMonth("");
setDay("");
setYear("");
};
const isDateDisabled = () => {
if (disableOptions) {
return disableOptions;
} else {
return false;
}
};
const monthForDropDown = [{ value: "", label: "<Month>" }, ...months];
const daysForDrop = [{ value: "", label: "<Day>" }, ...days];
const yearsForDrop = [{ value: "", label: "<Year>" }, ...years];
return (
<Box className={"inputItem"}>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 5,
marginBottom: 5,
}}
>
<InputLabel htmlFor={id}>
<span>{label}</span>
{tooltip !== "" && (
<Box
sx={{
marginLeft: 5,
display: "flex",
alignItems: "center",
"& .min-icon": {
width: 13,
},
}}
>
<Tooltip tooltip={tooltip} placement="top">
<Box
sx={{
"& .min-icon": {
width: 13,
},
}}
>
<HelpIcon />
</Box>
</Tooltip>
</Box>
)}
</InputLabel>
</Box>
<Box sx={{ display: "flex", gap: 12 }}>
<Select
id={`${id}-month`}
name={`${id}-month`}
value={month}
onChange={(newValue) => {
setMonth(newValue);
}}
options={monthForDropDown}
label={""}
disabled={isDateDisabled()}
/>
<Select
id={`${id}-day`}
name={`${id}-day`}
value={day}
onChange={(newValue) => {
setDay(newValue);
}}
options={daysForDrop}
label={""}
disabled={isDateDisabled()}
/>
<Select
id={`${id}-year`}
name={`${id}-year`}
value={year}
onChange={(newValue) => {
setYear(newValue);
}}
options={yearsForDrop}
label={""}
disabled={isDateDisabled()}
sx={{
marginBottom: 12,
}}
/>
</Box>
</Box>
);
},
);
export default DateSelector;

View File

@@ -1,67 +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/>.
export const months = [
{ value: "01", label: "January" },
{ value: "02", label: "February" },
{ value: "03", label: "March" },
{ value: "04", label: "April" },
{ value: "05", label: "May" },
{ value: "06", label: "June" },
{ value: "07", label: "July" },
{ value: "08", label: "August" },
{ value: "09", label: "September" },
{ value: "10", label: "October" },
{ value: "11", label: "November" },
{ value: "12", label: "December" },
];
export const days = Array.from(Array(31), (_, num) => ({
value: (num + 1).toString(),
label: (num + 1).toString(),
}));
const currentYear = new Date().getFullYear();
export const years = Array.from(Array(50), (_, numYear) => ({
value: (numYear + currentYear).toString(),
label: (numYear + currentYear).toString(),
}));
export const validDate = (year: string, month: string, day: string): any[] => {
const currentDate = Date.parse(`${year}-${month}-${day}`);
if (isNaN(currentDate)) {
return [false, ""];
}
const parsedMonth = parseInt(month);
const parsedDay = parseInt(day);
const monthForString = parsedMonth < 10 ? `0${parsedMonth}` : parsedMonth;
const dayForString = parsedDay < 10 ? `0${parsedDay}` : parsedDay;
const parsedDate = new Date(currentDate).toISOString().split("T")[0];
const dateString = `${year}-${monthForString}-${dayForString}`;
return [parsedDate === dateString, dateString];
};
// twoDigitDate gets a two digit string number used for months or days
// returns "NaN" if number is NaN
export const twoDigitDate = (num: number): string => {
return num < 10 ? `0${num}` : `${num}`;
};

View File

@@ -1,66 +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 } from "react";
import { InputBox, InputLabel, Box } from "mds";
interface IFilterInputWrapper {
value: string;
onChange: (txtVar: string) => any;
label: string;
placeholder?: string;
id: string;
name: string;
}
const FilterInputWrapper = ({
label,
onChange,
value,
placeholder = "",
id,
name,
}: IFilterInputWrapper) => {
return (
<Fragment>
<Box
sx={{
flexGrow: 1,
margin: "0 15px",
}}
>
<InputLabel>{label}</InputLabel>
<InputBox
placeholder={placeholder}
id={id}
name={name}
label=""
onChange={(val) => {
onChange(val.target.value);
}}
sx={{
"& input": {
height: 30,
},
}}
value={value}
/>
</Box>
</Fragment>
);
};
export default FilterInputWrapper;

View File

@@ -1,86 +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 } from "react";
import { DropdownSelector, SelectorType } from "mds";
import styled from "styled-components";
import get from "lodash/get";
interface IInputUnitBox {
id: string;
unitSelected: string;
unitsList: SelectorType[];
disabled?: boolean;
onUnitChange?: (newValue: string) => void;
}
const UnitMenuButton = styled.button(({ theme }) => ({
border: `1px solid ${get(theme, "borderColor", "#E2E2E2")}`,
borderRadius: 3,
color: get(theme, "secondaryText", "#5B5C5C"),
backgroundColor: get(theme, "boxBackground", "#FBFAFA"),
fontSize: 12,
}));
const InputUnitMenu = ({
id,
unitSelected,
unitsList,
disabled = false,
onUnitChange,
}: IInputUnitBox) => {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = (newUnit: string) => {
setAnchorEl(null);
if (newUnit !== "" && onUnitChange) {
onUnitChange(newUnit);
}
};
return (
<Fragment>
<UnitMenuButton
id={`${id}-button`}
aria-controls={`${id}-menu`}
aria-haspopup="true"
aria-expanded={open ? "true" : undefined}
onClick={handleClick}
disabled={disabled}
type={"button"}
>
{unitSelected}
</UnitMenuButton>
<DropdownSelector
id={"upload-main-menu"}
options={unitsList}
selectedOption={""}
onSelect={(value) => handleClose(value)}
hideTriggerAction={() => {
handleClose("");
}}
open={open}
anchorEl={anchorEl}
anchorOrigin={"end"}
/>
</Fragment>
);
};
export default InputUnitMenu;

View File

@@ -1,257 +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, {
ChangeEvent,
createRef,
Fragment,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
import get from "lodash/get";
import debounce from "lodash/debounce";
import {
AddIcon,
Box,
Grid,
HelpIcon,
InputBox,
InputLabel,
Tooltip,
} from "mds";
interface IQueryMultiSelector {
elements: string;
name: string;
label: string;
tooltip?: string;
keyPlaceholder?: string;
valuePlaceholder?: string;
withBorder?: boolean;
onChange: (elements: string) => void;
}
const QueryMultiSelector = ({
elements,
name,
label,
tooltip = "",
keyPlaceholder = "",
valuePlaceholder = "",
onChange,
withBorder = false,
}: IQueryMultiSelector) => {
const [currentKeys, setCurrentKeys] = useState<string[]>([""]);
const [currentValues, setCurrentValues] = useState<string[]>([""]);
const bottomList = createRef<HTMLDivElement>();
// Use effect to get the initial values from props
useEffect(() => {
if (
currentKeys.length === 1 &&
currentKeys[0] === "" &&
currentValues.length === 1 &&
currentValues[0] === "" &&
elements &&
elements !== ""
) {
const elementsSplit = elements.split("&");
let keys = [];
let values = [];
elementsSplit.forEach((element: string) => {
const splittedVals = element.split("=");
if (splittedVals.length === 2) {
keys.push(splittedVals[0]);
values.push(splittedVals[1]);
}
});
keys.push("");
values.push("");
setCurrentKeys(keys);
setCurrentValues(values);
}
}, [currentKeys, currentValues, elements]);
// Use effect to send new values to onChange
useEffect(() => {
const refScroll = bottomList.current;
if (refScroll && currentKeys.length > 1) {
refScroll.scrollIntoView(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentKeys]);
// We avoid multiple re-renders / hang issue typing too fast
const firstUpdate = useRef(true);
useLayoutEffect(() => {
if (firstUpdate.current) {
firstUpdate.current = false;
return;
}
debouncedOnChange();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentKeys, currentValues]);
// If the last input is not empty, we add a new one
const addEmptyLine = () => {
if (
currentKeys[currentKeys.length - 1].trim() !== "" &&
currentValues[currentValues.length - 1].trim() !== ""
) {
const keysList = [...currentKeys];
const valuesList = [...currentValues];
keysList.push("");
valuesList.push("");
setCurrentKeys(keysList);
setCurrentValues(valuesList);
}
};
// Onchange function for input box, we get the dataset-index & only update that value in the array
const onChangeKey = (e: ChangeEvent<HTMLInputElement>) => {
e.persist();
let updatedElement = [...currentKeys];
const index = get(e.target, "dataset.index", "0");
const indexNum = parseInt(index);
updatedElement[indexNum] = e.target.value;
setCurrentKeys(updatedElement);
};
const onChangeValue = (e: ChangeEvent<HTMLInputElement>) => {
e.persist();
let updatedElement = [...currentValues];
const index = get(e.target, "dataset.index", "0");
const indexNum = parseInt(index);
updatedElement[indexNum] = e.target.value;
setCurrentValues(updatedElement);
};
// Debounce for On Change
const debouncedOnChange = debounce(() => {
let queryString = "";
currentKeys.forEach((keyVal, index) => {
if (currentKeys[index] && currentValues[index]) {
let insertString = `${keyVal}=${currentValues[index]}`;
if (index !== 0) {
insertString = `&${insertString}`;
}
queryString = `${queryString}${insertString}`;
}
});
onChange(queryString);
}, 500);
const inputs = currentValues.map((element, index) => {
return (
<Grid
item
xs={12}
className={"lineInputBoxes inputItem"}
key={`query-pair-${name}-${index.toString()}`}
>
<InputBox
id={`${name}-key-${index.toString()}`}
label={""}
name={`${name}-${index.toString()}`}
value={currentKeys[index]}
onChange={onChangeKey}
index={index}
placeholder={keyPlaceholder}
/>
<span className={"queryDiv"}>:</span>
<InputBox
id={`${name}-value-${index.toString()}`}
label={""}
name={`${name}-${index.toString()}`}
value={currentValues[index]}
onChange={onChangeValue}
index={index}
placeholder={valuePlaceholder}
overlayIcon={index === currentValues.length - 1 ? <AddIcon /> : null}
overlayAction={() => {
addEmptyLine();
}}
/>
</Grid>
);
});
return (
<Fragment>
<Grid
item
xs={12}
sx={{
"& .lineInputBoxes": {
display: "flex",
},
"& .queryDiv": {
alignSelf: "center",
margin: "-15px 4px 0",
fontWeight: 600,
},
}}
className={"inputItem"}
>
<InputLabel>
{label}
{tooltip !== "" && (
<Box
sx={{
marginLeft: 5,
display: "flex",
alignItems: "center",
"& .min-icon": {
width: 13,
},
}}
>
<Tooltip tooltip={tooltip} placement="top">
<HelpIcon style={{ width: 13, height: 13 }} />
</Tooltip>
</Box>
)}
</InputLabel>
<Box
withBorders={withBorder}
sx={{
padding: 15,
height: 150,
overflowY: "auto",
position: "relative",
marginTop: 15,
}}
>
{inputs}
<div ref={bottomList} />
</Box>
</Grid>
</Fragment>
);
};
export default QueryMultiSelector;

View File

@@ -16,196 +16,6 @@
// This object contains variables that will be used across form components.
import { breakPoints } from "mds";
import get from "lodash/get";
export const modalBasic = {
formScrollable: {
maxHeight: "calc(100vh - 300px)" as const,
overflowY: "auto" as const,
marginBottom: 25,
},
clearButton: {
fontFamily: "Inter, sans-serif",
border: "0",
backgroundColor: "transparent",
color: "#393939",
fontWeight: 600,
fontSize: 14,
marginRight: 10,
outline: "0",
padding: "16px 25px 16px 25px",
cursor: "pointer",
},
configureString: {
border: "#EAEAEA 1px solid",
borderRadius: 4,
padding: "24px 50px",
overflowY: "auto" as const,
height: 170,
backgroundColor: "#FBFAFA",
},
};
export const actionsTray = {
actionsTray: {
display: "flex" as const,
justifyContent: "space-between" as const,
alignItems: "center",
marginBottom: "1rem",
"& button": {
flexGrow: 0,
marginLeft: 8,
},
},
};
export const typesSelection = {
iconContainer: {
display: "flex" as const,
flexDirection: "row" as const,
maxWidth: 1180,
justifyContent: "start" as const,
flexWrap: "wrap" as const,
width: "100%",
},
logoButton: {
height: "80px",
},
lambdaNotif: {
background: "#ffffff50",
border: "#E5E5E5 1px solid",
borderRadius: 5,
width: 250,
height: 80,
display: "flex",
alignItems: "center",
justifyContent: "start",
marginBottom: 16,
marginRight: 8,
cursor: "pointer",
padding: 0,
overflow: "hidden",
"&:hover": {
backgroundColor: "#ebebeb",
},
},
lambdaNotifIcon: {
background: "transparent",
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 80,
height: 80,
"& img": {
maxWidth: 46,
maxHeight: 46,
},
},
lambdaNotifTitle: {
color: "#07193E",
fontSize: 16,
fontFamily: "Inter,sans-serif",
paddingLeft: 18,
},
};
export const widgetCommon = (theme: any) => ({
"& .singleValueContainer": {
height: 200,
border: `${get(theme, "borderColor", "#eaeaea")} 1px solid`,
borderRadius: 2,
backgroundColor: get(theme, "bgColor", "#fff"),
padding: 16,
},
"& .titleContainer": {
color: get(theme, "mutedText", "#87888d"),
fontSize: 16,
fontWeight: 600,
paddingBottom: 14,
marginBottom: 5,
display: "flex" as const,
justifyContent: "space-between" as const,
},
"& .contentContainer": {
justifyContent: "center" as const,
alignItems: "center" as const,
display: "flex" as const,
width: "100%",
height: 140,
},
"& .singleLegendContainer": {
display: "flex",
alignItems: "center",
padding: "0 10px",
maxWidth: "100%",
},
"& .colorContainer": {
width: 8,
height: 8,
minWidth: 8,
marginRight: 5,
},
"& .legendLabel": {
fontSize: "80%",
color: get(theme, "mutedText", "#87888d"),
whiteSpace: "nowrap" as const,
overflow: "hidden" as const,
textOverflow: "ellipsis" as const,
},
"& .zoomChartCont": {
position: "relative" as const,
height: 340,
width: "100%",
},
});
export const tooltipCommon = {
customTooltip: {
backgroundColor: "rgba(255, 255, 255, 0.90)",
border: "#eaeaea 1px solid",
borderRadius: 3,
padding: "5px 10px",
maxHeight: 300,
overflowY: "auto" as const,
},
labelContainer: {
display: "flex" as const,
alignItems: "center" as const,
},
labelColor: {
width: 6,
height: 6,
display: "block" as const,
borderRadius: "100%",
marginRight: 5,
},
itemValue: {
fontSize: "75%",
color: "#393939",
},
valueContainer: {
fontWeight: 600,
},
timeStampTitle: {
fontSize: "80%",
color: "#9c9c9c",
textAlign: "center" as const,
marginBottom: 6,
},
};
export const formFieldStyles: any = {
formFieldRow: {
marginBottom: ".8rem",
"& .MuiInputLabel-root": {
fontWeight: "normal",
},
},
};
export const modalStyleUtils: any = {
modalButtonBar: {
marginTop: 15,
@@ -220,14 +30,3 @@ export const modalStyleUtils: any = {
paddingTop: 10,
},
};
export const twoColCssGridLayoutConfig = {
display: "grid",
gridTemplateColumns: "2fr 1fr",
gridAutoFlow: "row",
gap: 10,
[`@media (max-width: ${breakPoints.sm}px)`]: {
gridTemplateColumns: "1fr",
gridAutoFlow: "dense",
},
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,61 +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 } from "react";
import { HelpBox, Grid } from "mds";
interface IMissingIntegration {
iconComponent: any;
entity: string;
documentationLink: string;
}
const MissingIntegration = ({
iconComponent,
entity,
documentationLink,
}: IMissingIntegration) => {
return (
<Grid
container
sx={{
justifyContent: "center",
alignContent: "center",
alignItems: "center",
}}
>
<Grid item xs={8}>
<HelpBox
title={`${entity} not available`}
iconComponent={iconComponent}
help={
<Fragment>
This feature is not available.
<br />
Please configure{" "}
<a href={documentationLink} target="_blank" rel="noopener">
{entity}
</a>{" "}
first to use this feature.
</Fragment>
}
/>
</Grid>
</Grid>
);
};
export default MissingIntegration;

View File

@@ -1,34 +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 styled from "styled-components";
const PanelTitleContainer = styled.h1(() => ({
padding: 0,
margin: 0,
fontSize: ".9rem",
}));
interface IPanelTitle {
children: React.ReactNode;
}
const PanelTitle = ({ children }: IPanelTitle) => {
return <PanelTitleContainer>{children}</PanelTitleContainer>;
};
export default PanelTitle;

View File

@@ -69,6 +69,7 @@ const VirtualizedList = ({
width={width}
ref={ref}
onItemsRendered={onItemsRendered}
className={"bucketsListing"}
>
{RenderItemLine}
</List>

View File

@@ -1,57 +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 { useLocation } from "react-router-dom";
import { Grid } from "mds";
import { configurationElements } from "../utils";
import EditConfiguration from "../../EventDestinations/CustomForms/EditConfiguration";
const ConfigurationsList = () => {
const { pathname = "" } = useLocation();
const configName = pathname.substring(pathname.lastIndexOf("/") + 1);
const validActiveConfig = configurationElements.find(
(element) => element.configuration_id === configName,
);
const containerClassName = `${configName}`;
return (
<Grid
item
xs={12}
sx={{
height: "100%",
//LDAP and api forms have longer labels
"& .identity_ldap, .api": {
"& label": {
minWidth: 220,
marginRight: 0,
},
},
}}
>
{validActiveConfig && (
<EditConfiguration
className={`${containerClassName}`}
selectedConfiguration={validActiveConfig}
/>
)}
</Grid>
);
};
export default ConfigurationsList;

View File

@@ -1,187 +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, useCallback, useEffect, useState } from "react";
import {
Box,
Grid,
HelpBox,
PageLayout,
ScreenTitle,
SettingsIcon,
Tabs,
} from "mds";
import { configurationElements } from "../utils";
import {
Navigate,
Route,
Routes,
useLocation,
useNavigate,
} from "react-router-dom";
import ConfigurationForm from "./ConfigurationForm";
import { IAM_PAGES } from "../../../../common/SecureComponent/permissions";
import PageHeaderWrapper from "../../Common/PageHeaderWrapper/PageHeaderWrapper";
import ExportConfigButton from "./ExportConfigButton";
import ImportConfigButton from "./ImportConfigButton";
import HelpMenu from "../../HelpMenu";
import { setErrorSnackMessage, setHelpName } from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import { api } from "../../../../api";
import { IElement } from "../types";
import { errorToHandler } from "../../../../api/errors";
const getRoutePath = (path: string) => {
return `${IAM_PAGES.SETTINGS}/${path}`;
};
// region is not part of config subsystem list.
const NON_SUB_SYS_CONFIG_ITEMS = ["region"];
const IGNORED_CONFIG_SUB_SYS = ["cache"]; // cache config is not supported.
const ConfigurationOptions = () => {
const { pathname = "" } = useLocation();
const dispatch = useAppDispatch();
const navigate = useNavigate();
const [configSubSysList, setConfigSubSysList] = useState<string[]>([]);
const fetchConfigSubSysList = useCallback(async () => {
api.configs
.listConfig() // get a list of available config subsystems.
.then((res) => {
if (res && res?.data && res?.data?.configurations) {
const confSubSysList = (res?.data?.configurations || []).reduce(
(acc: string[], { key = "" }) => {
if (!IGNORED_CONFIG_SUB_SYS.includes(key)) {
acc.push(key);
}
return acc;
},
[],
);
setConfigSubSysList(confSubSysList);
}
})
.catch((err) => {
dispatch(setErrorSnackMessage(errorToHandler(err)));
});
}, [dispatch]);
useEffect(() => {
fetchConfigSubSysList();
dispatch(setHelpName("settings_Region"));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const availableConfigSubSys = configurationElements.filter(
({ configuration_id }: IElement) => {
return (
NON_SUB_SYS_CONFIG_ITEMS.includes(configuration_id) ||
configSubSysList.includes(configuration_id) ||
!configSubSysList.length
);
},
);
return (
<Fragment>
<PageHeaderWrapper label={"Configuration"} actions={<HelpMenu />} />
<PageLayout>
<Grid item xs={12} id={"settings-container"}>
<ScreenTitle
icon={<SettingsIcon />}
title={"MinIO Configuration:"}
actions={
<Box
sx={{
display: "flex",
gap: 10,
}}
>
<ImportConfigButton />
<ExportConfigButton />
</Box>
}
sx={{ marginBottom: 15 }}
/>
<Tabs
currentTabOrPath={pathname}
onTabClick={(path) => {
navigate(path);
}}
useRouteTabs
options={availableConfigSubSys.map((element) => {
const { configuration_id, configuration_label, icon } = element;
return {
tabConfig: {
id: `settings-tab-${configuration_label}`,
label: configuration_label,
value: configuration_id,
icon: icon,
to: getRoutePath(configuration_id),
},
};
})}
routes={
<Routes>
{availableConfigSubSys.map((element) => (
<Route
key={`configItem-${element.configuration_label}`}
path={`${element.configuration_id}`}
element={<ConfigurationForm />}
/>
))}
<Route
path={"/"}
element={<Navigate to={`${IAM_PAGES.SETTINGS}/region`} />}
/>
</Routes>
}
/>
</Grid>
<Grid item xs={12} sx={{ paddingTop: "15px" }}>
<HelpBox
title={"Learn more about Configurations"}
iconComponent={<SettingsIcon />}
help={
<Fragment>
MinIO supports a variety of configurations ranging from
encryption, compression, region, notifications, etc.
<br />
<br />
You can learn more at our{" "}
<a
href="https://min.io/docs/minio/linux/reference/minio-mc-admin/mc-admin-config.html?ref=con#id4"
target="_blank"
rel="noopener"
>
documentation
</a>
.
</Fragment>
}
/>
</Grid>
</PageLayout>
</Fragment>
);
};
export default ConfigurationOptions;

View File

@@ -1,59 +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 { Button, UploadIcon } from "mds";
import useApi from "../../Common/Hooks/useApi";
import { performDownload } from "../../../../common/utils";
import { DateTime } from "luxon";
import { setErrorSnackMessage } from "../../../../systemSlice";
import { useDispatch } from "react-redux";
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
const ExportConfigButton = () => {
const dispatch = useDispatch();
const [isReqLoading, invokeApi] = useApi(
(res: any) => {
//base64 encoded information so decode before downloading.
performDownload(
new Blob([window.atob(res.value)]),
`minio-server-config-${DateTime.now().toFormat(
"LL-dd-yyyy-HH-mm-ss",
)}.conf`,
);
},
(err) => {
dispatch(setErrorSnackMessage(err));
},
);
return (
<TooltipWrapper tooltip="Warning! The resulting file will contain server configuration information in plain text">
<Button
id={"export-config"}
onClick={() => {
invokeApi("GET", `api/v1/configs/export`);
}}
icon={<UploadIcon />}
label={"Export"}
variant={"regular"}
disabled={isReqLoading}
/>
</TooltipWrapper>
);
};
export default ExportConfigButton;

View File

@@ -1,107 +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, useRef, useState } from "react";
import { Button, DownloadIcon } from "mds";
import useApi from "../../Common/Hooks/useApi";
import {
setErrorSnackMessage,
setServerNeedsRestart,
} from "../../../../systemSlice";
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { AppState } from "../../../../store";
const ImportConfigButton = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const needsRestart = useSelector(
(state: AppState) => state.system.serverNeedsRestart,
);
const [refreshPage, setRefreshPage] = useState<boolean | undefined>(
undefined,
);
const fileUpload = useRef<HTMLInputElement>(null);
const [isReqLoading, invokeApi] = useApi(
(res: any) => {
//base64 encoded information so decode before downloading.
dispatch(setServerNeedsRestart(true)); //import should refreshPage as per mc.
setRefreshPage(true);
},
(err) => {
dispatch(setErrorSnackMessage(err));
},
);
useEffect(() => {
if (!needsRestart && refreshPage) {
navigate(0); // refresh the page.
}
}, [needsRestart, refreshPage, navigate]);
const handleUploadButton = (e: any) => {
if (
e === null ||
e === undefined ||
e.target.files === null ||
e.target.files === undefined
) {
return;
}
e.preventDefault();
const [fileToUpload] = e.target.files;
const formData = new FormData();
const blobFile = new Blob([fileToUpload], { type: fileToUpload.type });
formData.append("file", blobFile, fileToUpload.name);
// @ts-ignore
invokeApi("POST", `api/v1/configs/import`, formData);
e.target.value = "";
};
return (
<Fragment>
<input
type="file"
onChange={handleUploadButton}
style={{ display: "none" }}
ref={fileUpload}
/>
<TooltipWrapper tooltip="The file must be valid and should have valid config values">
<Button
id={"import-config"}
onClick={() => {
if (fileUpload && fileUpload.current) {
fileUpload.current.click();
}
}}
icon={<DownloadIcon />}
label={"Import"}
variant={"regular"}
disabled={isReqLoading}
/>
</TooltipWrapper>
</Fragment>
);
};
export default ImportConfigButton;

View File

@@ -1,80 +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 { SelectorTypes } from "../../../common/types";
import { EnvOverride } from "../../../api/consoleApi";
type KVFieldType =
| "string"
| "password"
| "number"
| "on|off"
| "enum"
| "path"
| "url"
| "address"
| "duration"
| "uri"
| "sentence"
| "csv"
| "comment"
| "switch";
export interface KVField {
name: string;
label: string;
tooltip: string;
required?: boolean;
type: KVFieldType;
options?: SelectorTypes[];
multiline?: boolean;
placeholder?: string;
withBorder?: boolean;
customValueProcess?: (value: string) => string;
}
export interface IConfigurationElement {
configuration_id: string;
configuration_label: string;
url?: string;
}
export interface IElementValue {
key: string;
value: string;
env_override?: EnvOverride;
}
export interface IConfigurationSys {
name?: string;
key_values: IElementValue[];
}
export interface IElement {
configuration_id: string;
configuration_label: string;
icon?: any;
disabled?: boolean;
}
export interface OverrideValue {
value: string;
overrideEnv: string;
}
export interface IOverrideEnv {
[key: string]: OverrideValue;
}

View File

@@ -1,441 +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 { IElement, IElementValue, IOverrideEnv, OverrideValue } from "./types";
import {
CodeIcon,
CompressIcon,
ConsoleIcon,
FindReplaceIcon,
FirstAidIcon,
KeyIcon,
LogsIcon,
PendingItemsIcon,
PublicIcon,
} from "mds";
export const configurationElements: IElement[] = [
{
icon: <PublicIcon />,
configuration_id: "region",
configuration_label: "Region",
},
{
icon: <CompressIcon />,
configuration_id: "compression",
configuration_label: "Compression",
},
{
icon: <CodeIcon />,
configuration_id: "api",
configuration_label: "API",
},
{
icon: <FirstAidIcon />,
configuration_id: "heal",
configuration_label: "Heal",
},
{
icon: <FindReplaceIcon />,
configuration_id: "scanner",
configuration_label: "Scanner",
},
{
icon: <KeyIcon />,
configuration_id: "etcd",
configuration_label: "Etcd",
},
{
icon: <ConsoleIcon />,
configuration_id: "logger_webhook",
configuration_label: "Logger Webhook",
},
{
icon: <PendingItemsIcon />,
configuration_id: "audit_webhook",
configuration_label: "Audit Webhook",
},
{
icon: <LogsIcon />,
configuration_id: "audit_kafka",
configuration_label: "Audit Kafka",
},
];
export const fieldsConfigurations: any = {
region: [
{
name: "name",
required: true,
label: "Server Location",
tooltip: 'Name of the location of the server e.g. "us-west-rack2"',
type: "string",
placeholder: "e.g. us-west-rack-2",
},
{
name: "comment",
required: false,
label: "Comment",
tooltip: "You can add a comment to this setting",
type: "comment",
placeholder: "Enter custom notes if any",
},
],
compression: [
{
name: "extensions",
required: false,
label: "Extensions",
tooltip:
'Extensions to compress e.g. ".txt", ".log" or ".csv" - you can write one per field',
type: "csv",
placeholder: "Enter an Extension",
withBorder: true,
},
{
name: "mime_types",
required: false,
label: "Mime Types",
tooltip:
'Mime types e.g. "text/*", "application/json" or "application/xml" - you can write one per field',
type: "csv",
placeholder: "Enter a Mime Type",
withBorder: true,
},
],
api: [
{
name: "requests_max",
required: false,
label: "Requests Max",
tooltip: "Maximum number of concurrent requests, e.g. '1600'",
type: "number",
placeholder: "Enter Requests Max",
},
{
name: "cors_allow_origin",
required: false,
label: "Cors Allow Origin",
tooltip: "List of origins allowed for CORS requests",
type: "csv",
placeholder: "Enter allowed origin e.g. https://example.com",
},
{
name: "replication_workers",
required: false,
label: "Replication Workers",
tooltip: "Number of replication workers, defaults to 100",
type: "number",
placeholder: "Enter Replication Workers",
},
{
name: "replication_failed_workers",
required: false,
label: "Replication Failed Workers",
tooltip:
"Number of replication workers for recently failed replicas, defaults to 4",
type: "number",
placeholder: "Enter Replication Failed Workers",
},
],
heal: [
{
name: "bitrotscan",
required: false,
label: "Bitrot Scan",
tooltip:
"Perform bitrot scan on disks when checking objects during scanner",
type: "on|off",
},
{
name: "max_sleep",
required: false,
label: "Max Sleep",
tooltip:
"Maximum sleep duration between objects to slow down heal operation, e.g. 2s",
type: "duration",
placeholder: "Enter Max Sleep Duration",
},
{
name: "max_io",
required: false,
label: "Max IO",
tooltip:
"Maximum IO requests allowed between objects to slow down heal operation, e.g. 3",
type: "number",
placeholder: "Enter Max IO",
},
],
scanner: [
{
name: "delay",
required: false,
label: "Delay Multiplier",
tooltip: "Scanner delay multiplier, defaults to '10.0'",
type: "number",
placeholder: "Enter Delay",
},
{
name: "max_wait",
required: false,
label: "Max Wait",
tooltip: "Maximum wait time between operations, defaults to '15s'",
type: "duration",
placeholder: "Enter Max Wait",
},
{
name: "cycle",
required: false,
label: "Cycle",
tooltip: "Time duration between scanner cycles, defaults to '1m'",
type: "duration",
placeholder: "Enter Cycle",
},
],
etcd: [
{
name: "endpoints",
required: true,
label: "Endpoints",
tooltip:
'List of etcd endpoints e.g. "http://localhost:2379" - you can write one per field',
type: "csv",
placeholder: "Enter Endpoint",
},
{
name: "path_prefix",
required: false,
label: "Path Prefix",
tooltip: 'Namespace prefix to isolate tenants e.g. "customer1/"',
type: "string",
placeholder: "Enter Path Prefix",
},
{
name: "coredns_path",
required: false,
label: "Coredns Path",
tooltip: 'Shared bucket DNS records, default is "/skydns"',
type: "string",
placeholder: "Enter Coredns Path",
},
{
name: "client_cert",
required: false,
label: "Client Cert",
tooltip: "Client cert for mTLS authentication",
type: "string",
placeholder: "Enter Client Cert",
},
{
name: "client_cert_key",
required: false,
label: "Client Cert Key",
tooltip: "Client cert key for mTLS authentication",
type: "string",
placeholder: "Enter Client Cert Key",
},
{
name: "comment",
required: false,
label: "Comment",
tooltip: "You can add a comment to this setting",
type: "comment",
multiline: true,
placeholder: "Enter custom notes if any",
},
],
logger_webhook: [
{
name: "endpoint",
required: true,
label: "Endpoint",
type: "string",
placeholder: "Enter Endpoint",
},
{
name: "auth_token",
required: true,
label: "Auth Token",
type: "string",
placeholder: "Enter Auth Token",
},
],
audit_webhook: [
{
name: "endpoint",
required: true,
label: "Endpoint",
type: "string",
placeholder: "Enter Endpoint",
},
{
name: "auth_token",
required: true,
label: "Auth Token",
type: "string",
placeholder: "Enter Auth Token",
},
],
audit_kafka: [
{
name: "enable",
required: false,
label: "Enable",
tooltip: "Enable audit_kafka target",
type: "on|off",
customValueProcess: (origValue: string) => {
return origValue === "" || origValue === "on" ? "on" : "off";
},
},
{
name: "brokers",
required: true,
label: "Brokers",
type: "csv",
placeholder: "Enter Kafka Broker",
},
{
name: "topic",
required: false,
label: "Topic",
type: "string",
placeholder: "Enter Kafka Topic",
tooltip: "Kafka topic used for bucket notifications",
},
{
name: "sasl",
required: false,
label: "Use SASL",
tooltip:
"Enable SASL (Simple Authentication and Security Layer) authentication",
type: "on|off",
},
{
name: "sasl_username",
required: false,
label: "SASL Username",
type: "string",
placeholder: "Enter SASL Username",
tooltip: "Username for SASL/PLAIN or SASL/SCRAM authentication",
},
{
name: "sasl_password",
required: false,
label: "SASL Password",
type: "password",
placeholder: "Enter SASL Password",
tooltip: "Password for SASL/PLAIN or SASL/SCRAM authentication",
},
{
name: "sasl_mechanism",
required: false,
label: "SASL Mechanism",
type: "string",
placeholder: "Enter SASL Mechanism",
tooltip: "SASL authentication mechanism",
},
{
name: "tls",
required: false,
label: "Use TLS",
tooltip: "Enable TLS (Transport Layer Security)",
type: "on|off",
},
{
name: "tls_skip_verify",
required: false,
label: "Skip TLS Verification",
tooltip: "Trust server TLS without verification",
type: "on|off",
},
{
name: "client_tls_cert",
required: false,
label: "Client Cert",
tooltip: "Client cert for mTLS authentication",
type: "string",
placeholder: "Enter Client Cert",
},
{
name: "client_tls_key",
required: false,
label: "Client Cert Key",
tooltip: "Client cert key for mTLS authentication",
type: "string",
placeholder: "Enter Client Cert Key",
},
{
name: "tls_client_auth",
required: false,
label: "TLS Client Auth",
tooltip:
"ClientAuth determines the Kafka server's policy for TLS client authorization",
type: "string",
},
{
name: "version",
required: false,
label: "Version",
tooltip: "Specify the version of the Kafka cluster",
type: "string",
},
],
};
export const removeEmptyFields = (formFields: IElementValue[]) => {
const nonEmptyFields = formFields.filter((field) => field.value !== "");
return nonEmptyFields;
};
export const selectSAs = (
e: React.ChangeEvent<HTMLInputElement>,
setSelectedSAs: Function,
selectedSAs: string[],
) => {
const targetD = e.target;
const value = targetD.value;
const checked = targetD.checked;
let elements: string[] = [...selectedSAs]; // We clone the selectedSAs array
if (checked) {
// If the user has checked this field we need to push this to selectedSAs
elements.push(value);
} else {
// User has unchecked this field, we need to remove it from the list
elements = elements.filter((element) => element !== value);
}
setSelectedSAs(elements);
return elements;
};
export const overrideFields = (formFields: IElementValue[]): IOverrideEnv => {
let overrideReturn: IOverrideEnv = {};
formFields.forEach((envItem) => {
// it has override values, we construct the value
if (envItem.env_override) {
const value: OverrideValue = {
value: envItem.env_override.value || "",
overrideEnv: envItem.env_override.name || "",
};
overrideReturn = { ...overrideReturn, [envItem.key]: value };
}
});
return overrideReturn;
};

View File

@@ -21,49 +21,25 @@ import React, {
useLayoutEffect,
useState,
} from "react";
import { Box, Button, MainContainer, ProgressBar, Snackbar } from "mds";
import { MainContainer, ProgressBar, Snackbar } from "mds";
import debounce from "lodash/debounce";
import { Navigate, Route, Routes, useLocation } from "react-router-dom";
import { useSelector } from "react-redux";
import { selFeatures, selSession } from "./consoleSlice";
import { api } from "api";
import { AppState, useAppDispatch } from "../../store";
import MainError from "./Common/MainError/MainError";
import {
CONSOLE_UI_RESOURCE,
IAM_PAGES,
IAM_PAGES_PERMISSIONS,
IAM_SCOPES,
S3_ALL_RESOURCES,
} from "../../common/SecureComponent/permissions";
import { hasPermission } from "../../common/SecureComponent";
import { IRouteRule } from "./Menu/types";
import {
menuOpen,
serverIsLoading,
setServerNeedsRestart,
setSnackBarMessage,
} from "../../systemSlice";
import { menuOpen, setSnackBarMessage } from "../../systemSlice";
import MenuWrapper from "./Menu/MenuWrapper";
import LoadingComponent from "../../common/LoadingComponent";
import ComponentsScreen from "./Common/ComponentsScreen";
const EventDestinations = React.lazy(
() => import("./EventDestinations/EventDestinations"),
);
const AddEventDestination = React.lazy(
() => import("./EventDestinations/AddEventDestination"),
);
const EventTypeSelector = React.lazy(
() => import("./EventDestinations/EventTypeSelector"),
);
const ErrorLogs = React.lazy(() => import("./Logs/ErrorLogs/ErrorLogs"));
const LogsSearchMain = React.lazy(
() => import("./Logs/LogSearch/LogsSearchMain"),
);
const GroupsDetails = React.lazy(() => import("./Groups/GroupsDetails"));
const IconsScreen = React.lazy(() => import("./Common/IconsScreen"));
import AddBucketModal from "./Buckets/ListBuckets/AddBucket/AddBucketModal";
const ObjectManager = React.lazy(
() => import("./Common/ObjectManager/ObjectManager"),
@@ -73,46 +49,7 @@ const ObjectBrowser = React.lazy(() => import("./ObjectBrowser/ObjectBrowser"));
const Buckets = React.lazy(() => import("./Buckets/Buckets"));
const EditBucketReplication = React.lazy(
() => import("./Buckets/BucketDetails/EditBucketReplication"),
);
const AddBucketReplication = React.lazy(
() => import("./Buckets/BucketDetails/AddBucketReplication"),
);
const Policies = React.lazy(() => import("./Policies/Policies"));
const AddPolicyScreen = React.lazy(() => import("./Policies/AddPolicyScreen"));
const Dashboard = React.lazy(() => import("./Dashboard/Dashboard"));
const Account = React.lazy(() => import("./Account/Account"));
const AccountCreate = React.lazy(
() => import("./Account/AddServiceAccountScreen"),
);
const Users = React.lazy(() => import("./Users/Users"));
const Groups = React.lazy(() => import("./Groups/Groups"));
const IDPOpenIDConfigurations = React.lazy(
() => import("./IDP/IDPOpenIDConfigurations"),
);
const AddIDPOpenIDConfiguration = React.lazy(
() => import("./IDP/AddIDPOpenIDConfiguration"),
);
const IDPLDAPConfigurationDetails = React.lazy(
() => import("./IDP/LDAP/IDPLDAPConfigurationDetails"),
);
const IDPOpenIDConfigurationDetails = React.lazy(
() => import("./IDP/IDPOpenIDConfigurationDetails"),
);
const License = React.lazy(() => import("./License/License"));
const ConfigurationOptions = React.lazy(
() => import("./Configurations/ConfigurationPanels/ConfigurationOptions"),
);
const AddGroupScreen = React.lazy(() => import("./Groups/AddGroupScreen"));
const KMSRoutes = React.lazy(() => import("./KMS/KMSRoutes"));
const Console = () => {
const dispatch = useAppDispatch();
@@ -123,45 +60,21 @@ const Console = () => {
const snackBarMessage = useSelector(
(state: AppState) => state.system.snackBar,
);
const needsRestart = useSelector(
(state: AppState) => state.system.serverNeedsRestart,
);
const isServerLoading = useSelector(
(state: AppState) => state.system.serverIsLoading,
);
const loadingProgress = useSelector(
(state: AppState) => state.system.loadingProgress,
);
const createBucketOpen = useSelector(
(state: AppState) => state.addBucket.addBucketOpen,
);
const [openSnackbar, setOpenSnackbar] = useState<boolean>(false);
const ldapIsEnabled = (features && features.includes("ldap-idp")) || false;
const kmsIsEnabled = (features && features.includes("kms")) || false;
const obOnly = !!features?.includes("object-browser-only");
useEffect(() => {
dispatch({ type: "socket/OBConnect" });
}, [dispatch]);
const restartServer = () => {
dispatch(serverIsLoading(true));
api.service
.restartService({})
.then(() => {
console.log("success restarting service");
dispatch(serverIsLoading(false));
dispatch(setServerNeedsRestart(false));
})
.catch((err) => {
if (err.error.errorMessage === "Error 502") {
dispatch(setServerNeedsRestart(false));
}
dispatch(serverIsLoading(false));
console.log("failure restarting service");
console.error(err.error);
});
};
// Layout effect to be executed after last re-render for resizing only
useLayoutEffect(() => {
// Debounce to not execute constantly
@@ -201,10 +114,6 @@ const Console = () => {
path: IAM_PAGES.BUCKETS,
forceDisplay: true,
},
{
component: Dashboard,
path: IAM_PAGES.DASHBOARD,
},
{
component: Buckets,
path: IAM_PAGES.ADD_BUCKETS,
@@ -212,141 +121,14 @@ const Console = () => {
return hasPermission("*", IAM_PAGES_PERMISSIONS[IAM_PAGES.ADD_BUCKETS]);
},
},
{
component: AddBucketReplication,
path: IAM_PAGES.BUCKETS_ADD_REPLICATION,
customPermissionFnc: () => {
return hasPermission(
"*",
IAM_PAGES_PERMISSIONS[IAM_PAGES.BUCKETS_ADD_REPLICATION],
);
},
},
{
component: EditBucketReplication,
path: IAM_PAGES.BUCKETS_EDIT_REPLICATION,
customPermissionFnc: () => {
return hasPermission(
"*",
IAM_PAGES_PERMISSIONS[IAM_PAGES.BUCKETS_EDIT_REPLICATION],
);
},
},
{
component: Buckets,
path: IAM_PAGES.BUCKETS_ADMIN_VIEW,
customPermissionFnc: () => {
const path = window.location.pathname;
const resource = path.match(/buckets\/(.*)\/admin*/);
return (
resource &&
resource.length > 0 &&
hasPermission(
resource[1],
IAM_PAGES_PERMISSIONS[IAM_PAGES.BUCKETS_ADMIN_VIEW],
)
);
},
},
{
component: Users,
path: IAM_PAGES.USERS,
fsHidden: ldapIsEnabled,
customPermissionFnc: () =>
hasPermission(CONSOLE_UI_RESOURCE, [IAM_SCOPES.ADMIN_LIST_USERS]) ||
hasPermission(S3_ALL_RESOURCES, [IAM_SCOPES.ADMIN_CREATE_USER]),
},
{
component: Groups,
path: IAM_PAGES.GROUPS,
fsHidden: ldapIsEnabled,
},
{
component: AddGroupScreen,
path: IAM_PAGES.GROUPS_ADD,
},
{
component: GroupsDetails,
path: IAM_PAGES.GROUPS_VIEW,
},
{
component: Policies,
path: IAM_PAGES.POLICIES_VIEW,
},
{
component: AddPolicyScreen,
path: IAM_PAGES.POLICY_ADD,
},
{
component: Policies,
path: IAM_PAGES.POLICIES,
},
{
component: IDPLDAPConfigurationDetails,
path: IAM_PAGES.IDP_LDAP_CONFIGURATIONS,
},
{
component: IDPOpenIDConfigurations,
path: IAM_PAGES.IDP_OPENID_CONFIGURATIONS,
},
{
component: AddIDPOpenIDConfiguration,
path: IAM_PAGES.IDP_OPENID_CONFIGURATIONS_ADD,
},
{
component: IDPOpenIDConfigurationDetails,
path: IAM_PAGES.IDP_OPENID_CONFIGURATIONS_VIEW,
},
{
component: ErrorLogs,
path: IAM_PAGES.TOOLS_LOGS,
},
{
component: LogsSearchMain,
path: IAM_PAGES.TOOLS_AUDITLOGS,
},
{
component: ConfigurationOptions,
path: IAM_PAGES.SETTINGS,
},
{
component: AddEventDestination,
path: IAM_PAGES.EVENT_DESTINATIONS_ADD_SERVICE,
},
{
component: EventTypeSelector,
path: IAM_PAGES.EVENT_DESTINATIONS_ADD,
},
{
component: EventDestinations,
path: IAM_PAGES.EVENT_DESTINATIONS,
},
{
component: Account,
path: IAM_PAGES.ACCOUNT,
forceDisplay: true,
// user has implicit access to service-accounts
},
{
component: AccountCreate,
path: IAM_PAGES.ACCOUNT_ADD,
forceDisplay: true, // user has implicit access to service-accounts
},
{
component: License,
path: IAM_PAGES.LICENSE,
forceDisplay: true,
},
{
component: KMSRoutes,
path: IAM_PAGES.KMS,
fsHidden: !kmsIsEnabled,
},
];
let routes = consoleAdminRoutes;
const allowedRoutes = routes.filter((route: any) =>
const allowedRoutes = consoleAdminRoutes.filter((route: any) =>
obOnly
? route.path.includes("browser")
: (route.forceDisplay ||
@@ -388,54 +170,6 @@ const Console = () => {
mobileModeAuto={false}
>
<Fragment>
{needsRestart && (
<Snackbar
onClose={() => {}}
open={needsRestart}
variant={"warning"}
message={
<Box
sx={{
display: "flex",
gap: 8,
justifyContent: "center",
alignItems: "center",
width: "100%",
}}
>
{isServerLoading ? (
<Fragment>
<ProgressBar
barHeight={3}
transparentBG
sx={{
width: "100%",
position: "absolute",
top: 0,
left: 0,
}}
/>
<span>The server is restarting.</span>
</Fragment>
) : (
<Fragment>
The instance needs to be restarted for configuration
changes to take effect.{" "}
<Button
id={"restart-server"}
variant="secondary"
onClick={() => {
restartServer();
}}
label={"Restart"}
/>
</Fragment>
)}
</Box>
}
autoHideDuration={0}
/>
)}
{loadingProgress < 100 && (
<ProgressBar
barHeight={3}
@@ -444,6 +178,7 @@ const Console = () => {
sx={{ width: "100%", position: "absolute", top: 0, left: 0 }}
/>
)}
{createBucketOpen && <AddBucketModal />}
<MainError />
<Snackbar
onClose={closeSnackBar}
@@ -468,24 +203,6 @@ const Console = () => {
}
/>
))}
<Route
key={"icons"}
path={"icons"}
element={
<Suspense fallback={<LoadingComponent />}>
<IconsScreen />
</Suspense>
}
/>
<Route
key={"components"}
path={"components"}
element={
<Suspense fallback={<LoadingComponent />}>
<ComponentsScreen />
</Suspense>
}
/>
<Route
path={"*"}
element={

View File

@@ -23,6 +23,7 @@ import { selFeatures } from "./consoleSlice";
import TrafficMonitor from "./Common/ObjectManager/TrafficMonitor";
import { AppState } from "../../store";
import AnonymousAccess from "../AnonymousAccess/AnonymousAccess";
import LicenseConsentModal from "./License/LicenseConsentModal";
const ConsoleKBar = () => {
const features = useSelector(selFeatures);
@@ -57,6 +58,7 @@ const ConsoleKBar = () => {
>
<TrafficMonitor />
<CommandBar />
<LicenseConsentModal />
<Console />
</KBarProvider>
);

View File

@@ -1,366 +0,0 @@
// 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 from "react";
import {
ArrowRightIcon,
Box,
breakPoints,
BucketsIcon,
Button,
DiagnosticsMenuIcon,
DrivesIcon,
FormatDrivesIcon,
HealIcon,
HelpBox,
PrometheusErrorIcon,
ServersIcon,
StorageIcon,
TotalObjectsIcon,
UptimeIcon,
} from "mds";
import { calculateBytes, representationNumber } from "../../../../common/utils";
import StatusCountCard from "./StatusCountCard";
import groupBy from "lodash/groupBy";
import ServersList from "./ServersList";
import CounterCard from "./CounterCard";
import ReportedUsage from "./ReportedUsage";
import { Link } from "react-router-dom";
import { IAM_PAGES } from "../../../../common/SecureComponent/permissions";
import TimeStatItem from "../TimeStatItem";
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
import { AdminInfoResponse, ServerDrives } from "api/consoleApi";
const BoxItem = ({ children }: { children: any }) => {
return (
<Box
withBorders
sx={{
padding: 15,
height: "136px",
maxWidth: "100%",
[`@media (max-width: ${breakPoints.sm}px)`]: {
padding: 5,
maxWidth: "initial",
},
}}
>
{children}
</Box>
);
};
interface IDashboardProps {
usage: AdminInfoResponse | undefined;
}
const getServersList = (usage: AdminInfoResponse | undefined) => {
if (usage && usage.servers) {
return [...usage.servers].sort(function (a, b) {
const nameA = a.endpoint?.toLowerCase() || "";
const nameB = b.endpoint?.toLowerCase() || "";
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
return 0;
});
}
return [];
};
const prettyUsage = (usage: string | undefined) => {
if (usage === undefined) {
return { total: "0", unit: "Mi" };
}
return calculateBytes(usage);
};
const BasicDashboard = ({ usage }: IDashboardProps) => {
const usageValue = usage && usage.usage ? usage.usage.toString() : "0";
const usageToRepresent = prettyUsage(usageValue);
const { lastScan = "n/a", lastHeal = "n/a", upTime = "n/a" } = {};
const serverList = getServersList(usage);
let allDrivesArray: ServerDrives[] = [];
serverList.forEach((server) => {
const drivesInput = server.drives?.map((drive) => {
return drive;
});
if (drivesInput) {
allDrivesArray = [...allDrivesArray, ...drivesInput];
}
});
const serversGroup = groupBy(serverList, "state");
const { offline: offlineServers = [], online: onlineServers = [] } =
serversGroup;
const drivesGroup = groupBy(allDrivesArray, "state");
const { offline: offlineDrives = [], ok: onlineDrives = [] } = drivesGroup;
return (
<Box>
<Box
sx={{
display: "grid",
gridTemplateRows: "1fr",
gridTemplateColumns: "1fr",
gap: 27,
marginBottom: 40,
}}
>
<Box
sx={{
display: "grid",
gridTemplateColumns: "1fr",
gap: "40px",
}}
>
<Box
sx={{
display: "grid",
gridTemplateRows: "136px",
gridTemplateColumns: "1fr 1fr 1fr",
gap: 20,
[`@media (max-width: ${breakPoints.sm}px)`]: {
gridTemplateColumns: "1fr",
},
[`@media (max-width: ${breakPoints.md}px)`]: {
marginBottom: 0,
},
}}
>
<BoxItem>
<CounterCard
label={"Buckets"}
icon={<BucketsIcon />}
counterValue={usage ? representationNumber(usage.buckets) : 0}
actions={
<Link
to={IAM_PAGES.BUCKETS}
style={{
zIndex: 3,
textDecoration: "none",
top: "40px",
position: "relative",
marginRight: "75px",
}}
>
<TooltipWrapper tooltip={"Browse"}>
<Button
id={"browse-dashboard"}
onClick={() => {}}
label={"Browse"}
icon={<ArrowRightIcon />}
variant={"regular"}
style={{
padding: 5,
height: 30,
fontSize: 14,
marginTop: 20,
}}
/>
</TooltipWrapper>
</Link>
}
/>
</BoxItem>
<BoxItem>
<CounterCard
label={"Objects"}
icon={<TotalObjectsIcon />}
counterValue={usage ? representationNumber(usage.objects) : 0}
/>
</BoxItem>
<BoxItem>
<StatusCountCard
onlineCount={onlineServers.length}
offlineCount={offlineServers.length}
label={"Servers"}
icon={<ServersIcon />}
/>
</BoxItem>
<BoxItem>
<StatusCountCard
offlineCount={
usage?.backend?.offlineDrives || offlineDrives.length
}
onlineCount={
usage?.backend?.onlineDrives || onlineDrives.length
}
label={"Drives"}
icon={<DrivesIcon />}
/>
</BoxItem>
<Box
withBorders
sx={{
gridRowStart: "1",
gridRowEnd: "3",
gridColumnStart: "3",
padding: 15,
display: "grid",
justifyContent: "stretch",
}}
>
<ReportedUsage
usageValue={usageValue}
total={usageToRepresent.total}
unit={usageToRepresent.unit}
/>
<Box
sx={{
display: "flex",
flexFlow: "column",
gap: "14px",
}}
>
<TimeStatItem
icon={<HealIcon />}
label={
<Box>
<Box
sx={{
display: "inline",
[`@media (max-width: ${breakPoints.sm}px)`]: {
display: "none",
},
}}
>
Time since last
</Box>{" "}
Heal Activity
</Box>
}
value={lastHeal}
/>
<TimeStatItem
icon={<DiagnosticsMenuIcon />}
label={
<Box>
<Box
sx={{
display: "inline",
[`@media (max-width: ${breakPoints.sm}px)`]: {
display: "none",
},
}}
>
Time since last
</Box>{" "}
Scan Activity
</Box>
}
value={lastScan}
/>
<TimeStatItem
icon={<UptimeIcon />}
label={"Uptime"}
value={upTime}
/>
</Box>
</Box>
</Box>
<Box
sx={{
display: "grid",
gridTemplateColumns: "1fr 1fr 1fr",
gap: "14px",
[`@media (max-width: ${breakPoints.lg}px)`]: {
gridTemplateColumns: "1fr",
},
}}
>
<TimeStatItem
icon={<StorageIcon />}
label={"Backend type"}
value={usage?.backend?.backendType ?? "Unknown"}
/>
<TimeStatItem
icon={<FormatDrivesIcon />}
label={"Standard storage class parity"}
value={usage?.backend?.standardSCParity?.toString() ?? "n/a"}
/>
<TimeStatItem
icon={<FormatDrivesIcon />}
label={"Reduced redundancy storage class parity"}
value={usage?.backend?.rrSCParity?.toString() ?? "n/a"}
/>
</Box>
<Box
sx={{
display: "grid",
gridTemplateRows: "auto",
gridTemplateColumns: "1fr",
gap: "auto",
}}
>
<ServersList data={serverList} />
</Box>
</Box>
{usage?.advancedMetricsStatus === "not configured" && (
<Box>
<HelpBox
iconComponent={<PrometheusErrorIcon />}
title={"We cant retrieve advanced metrics at this time."}
help={
<Box>
<Box
sx={{
fontSize: "14px",
}}
>
MinIO Dashboard will display basic metrics as we couldnt
connect to Prometheus successfully. Please try again in a
few minutes. If the problem persists, you can review your
configuration and confirm that Prometheus server is up and
running.
</Box>
<Box
sx={{
paddingTop: 20,
fontSize: 14,
}}
>
<a
href="https://min.io/docs/minio/linux/operations/monitoring/collect-minio-metrics-using-prometheus.html"
target="_blank"
rel="noopener"
>
Read more about Prometheus on our Docs site.
</a>
</Box>
</Box>
}
/>
</Box>
)}
</Box>
</Box>
);
};
export default BasicDashboard;

View File

@@ -1,136 +0,0 @@
// 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 from "react";
import get from "lodash/get";
import styled from "styled-components";
import { Box, breakPoints, Tooltip } from "mds";
const CounterCardMain = styled.div(({ theme }) => ({
fontFamily: "Inter,sans-serif",
color: get(theme, "signalColors.main", "#07193E"),
maxWidth: "300px",
display: "flex",
marginLeft: "auto",
marginRight: "auto",
cursor: "default",
position: "relative",
width: "100%",
}));
const CounterCard = ({
counterValue,
label = "",
icon = null,
actions = null,
}: {
counterValue: string | number;
label?: any;
icon?: any;
actions?: any;
}) => {
return (
<CounterCardMain>
<Box
sx={{
flex: 1,
display: "flex",
width: "100%",
padding: "0 8px 0 8px",
position: "absolute",
[`@media (max-width: ${breakPoints.md}px)`]: {
padding: "0 10px 0 10px",
},
}}
>
<Box
sx={{
flex: 1,
display: "flex",
flexFlow: "column",
marginTop: "8px",
zIndex: 10,
overflow: "hidden",
}}
>
<Box
sx={{
fontSize: "16px",
fontWeight: 600,
}}
>
{label}
</Box>
<Tooltip tooltip={counterValue} placement="bottom">
<Box
sx={{
fontWeight: 600,
overflow: "hidden",
textOverflow: "ellipsis",
maxWidth: 187,
flexFlow: "row",
fontSize: counterValue.toString().length >= 5 ? 50 : 55,
[`@media (max-width: ${breakPoints.sm}px)`]: {
flexFlow: "column",
maxWidth: 200,
fontSize: counterValue.toString().length >= 5 ? 20 : 35,
},
[`@media (max-width: ${breakPoints.md}px)`]: {
fontSize: counterValue.toString().length >= 5 ? 28 : 35,
},
[`@media (max-width: ${breakPoints.lg}px)`]: {
fontSize: counterValue.toString().length >= 5 ? 28 : 36,
},
[`@media (max-width: ${breakPoints.xl}px)`]: {
fontSize: counterValue.toString().length >= 5 ? 45 : 50,
},
}}
>
{counterValue}
</Box>
</Tooltip>
</Box>
<Box
sx={{
display: "flex",
flexFlow: "column",
alignItems: "center",
justifyContent: "flex-start",
marginTop: "8px",
maxWidth: "26px",
"& .min-icon": {
width: "16px",
height: "16px",
},
}}
>
{icon}
<Box
sx={{
display: "flex",
}}
>
{actions}
</Box>
</Box>
</Box>
</CounterCardMain>
);
};
export default CounterCard;

Some files were not shown because too many files have changed in this diff Show More