mirror of
https://github.com/OpenMaxIO/openmaxio-object-browser
synced 2026-07-01 07:41:18 -07:00
Implemented AGPL MinIO Object Browser simplified Console (#3509)
Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
@@ -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();
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -65,8 +65,3 @@ export const getLogoApplicationVariant =
|
||||
return "console";
|
||||
}
|
||||
};
|
||||
|
||||
export const registeredCluster = (): boolean => {
|
||||
const plan = getLogoVar();
|
||||
return ["standard", "enterprise", "enterpriseos"].includes(plan || "AGPL");
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 it’s 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;
|
||||
@@ -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" },
|
||||
];
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
<ActionLink
|
||||
onClick={() => {
|
||||
navigate(IAM_PAGES.ADD_BUCKETS);
|
||||
}}
|
||||
>
|
||||
Create a Bucket.
|
||||
</ActionLink>
|
||||
</SecureComponent>
|
||||
</Fragment>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
</PageLayout>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListBuckets;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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}`;
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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;
|
||||
@@ -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;
|
||||
@@ -69,6 +69,7 @@ const VirtualizedList = ({
|
||||
width={width}
|
||||
ref={ref}
|
||||
onItemsRendered={onItemsRendered}
|
||||
className={"bucketsListing"}
|
||||
>
|
||||
{RenderItemLine}
|
||||
</List>
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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={
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 can’t retrieve advanced metrics at this time."}
|
||||
help={
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
MinIO Dashboard will display basic metrics as we couldn’t
|
||||
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;
|
||||
@@ -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
Reference in New Issue
Block a user