Removed Tools support (#3467)

- Removed Menu links for Support tools
- Removed support in UI for registering cluster
- Removed Subnet support
- Removed Websockets for tools support
- Removed Support endpoint
- Removed Subnet support endpoints

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2024-11-05 14:56:53 -06:00
committed by GitHub
parent d425af3c85
commit 18e50975d4
127 changed files with 28 additions and 15228 deletions

View File

@@ -22,7 +22,6 @@
"react-pdf": "^9.1.0",
"react-redux": "^8.1.3",
"react-router-dom": "6.25.1",
"react-use-websocket": "^4.8.1",
"react-virtualized": "^9.22.5",
"react-window": "^1.8.10",
"react-window-infinite-loader": "^1.0.9",

View File

@@ -1236,43 +1236,6 @@ export interface Metadata {
objectMetadata?: Record<string, any>;
}
export interface SubnetLoginResponse {
access_token?: string;
organizations?: SubnetOrganization[];
mfa_token?: string;
registered?: boolean;
}
export interface SubnetLoginRequest {
username?: string;
password?: string;
apiKey?: string;
}
export interface SubnetLoginMFARequest {
username: string;
otp: string;
mfa_token: string;
}
export interface SubnetRegisterRequest {
token: string;
account_id: string;
}
export interface SubnetRegTokenResponse {
regToken?: string;
}
export interface SubnetOrganization {
userId?: number;
accountId?: number;
subscriptionStatus?: string;
isAccountOwner?: boolean;
company?: string;
shortName?: string;
}
export interface PermissionResource {
resource?: string;
conditionOperator?: string;
@@ -3997,164 +3960,6 @@ export class Api<
...params,
}),
};
profiling = {
/**
* No description
*
* @tags Profile
* @name ProfilingStart
* @summary Start recording profile data
* @request POST:/profiling/start
* @secure
*/
profilingStart: (body: ProfilingStartRequest, params: RequestParams = {}) =>
this.request<StartProfilingList, ApiError>({
path: `/profiling/start`,
method: "POST",
body: body,
secure: true,
type: ContentType.Json,
format: "json",
...params,
}),
/**
* No description
*
* @tags Profile
* @name ProfilingStop
* @summary Stop and download profile data
* @request POST:/profiling/stop
* @secure
*/
profilingStop: (params: RequestParams = {}) =>
this.request<File, ApiError>({
path: `/profiling/stop`,
method: "POST",
secure: true,
...params,
}),
};
subnet = {
/**
* No description
*
* @tags Subnet
* @name SubnetRegToken
* @summary SUBNET registraton token
* @request GET:/subnet/registration-token
* @secure
*/
subnetRegToken: (params: RequestParams = {}) =>
this.request<SubnetRegTokenResponse, ApiError>({
path: `/subnet/registration-token`,
method: "GET",
secure: true,
format: "json",
...params,
}),
/**
* No description
*
* @tags Subnet
* @name SubnetInfo
* @summary Subnet info
* @request GET:/subnet/info
* @secure
*/
subnetInfo: (params: RequestParams = {}) =>
this.request<License, ApiError>({
path: `/subnet/info`,
method: "GET",
secure: true,
format: "json",
...params,
}),
/**
* No description
*
* @tags Subnet
* @name SubnetApiKey
* @summary Subnet api key
* @request GET:/subnet/apikey
* @secure
*/
subnetApiKey: (
query: {
token: string;
},
params: RequestParams = {},
) =>
this.request<ApiKey, ApiError>({
path: `/subnet/apikey`,
method: "GET",
query: query,
secure: true,
format: "json",
...params,
}),
/**
* No description
*
* @tags Subnet
* @name SubnetRegister
* @summary Register cluster with Subnet
* @request POST:/subnet/register
* @secure
*/
subnetRegister: (body: SubnetRegisterRequest, params: RequestParams = {}) =>
this.request<void, ApiError>({
path: `/subnet/register`,
method: "POST",
body: body,
secure: true,
type: ContentType.Json,
...params,
}),
/**
* No description
*
* @tags Subnet
* @name SubnetLogin
* @summary Login to SUBNET
* @request POST:/subnet/login
* @secure
*/
subnetLogin: (body: SubnetLoginRequest, params: RequestParams = {}) =>
this.request<SubnetLoginResponse, ApiError>({
path: `/subnet/login`,
method: "POST",
body: body,
secure: true,
type: ContentType.Json,
format: "json",
...params,
}),
/**
* No description
*
* @tags Subnet
* @name SubnetLoginMfa
* @summary Login to SUBNET using mfa
* @request POST:/subnet/login/mfa
* @secure
*/
subnetLoginMfa: (body: SubnetLoginMFARequest, params: RequestParams = {}) =>
this.request<SubnetLoginResponse, ApiError>({
path: `/subnet/login/mfa`,
method: "POST",
body: body,
secure: true,
type: ContentType.Json,
format: "json",
...params,
}),
};
admin = {
/**
* No description
@@ -4975,44 +4780,6 @@ export class Api<
...params,
}),
};
support = {
/**
* No description
*
* @tags Support
* @name GetCallHomeOptionValue
* @summary Get Callhome current status
* @request GET:/support/callhome
* @secure
*/
getCallHomeOptionValue: (params: RequestParams = {}) =>
this.request<CallHomeGetResponse, ApiError>({
path: `/support/callhome`,
method: "GET",
secure: true,
format: "json",
...params,
}),
/**
* No description
*
* @tags Support
* @name SetCallHomeStatus
* @summary Sets callhome status
* @request PUT:/support/callhome
* @secure
*/
setCallHomeStatus: (body: CallHomeSetStatus, params: RequestParams = {}) =>
this.request<void, ApiError>({
path: `/support/callhome`,
method: "PUT",
body: body,
secure: true,
type: ContentType.Json,
...params,
}),
};
downloadSharedObject = {
/**
* No description

View File

@@ -183,15 +183,6 @@ export const IAM_PAGES = {
KMS_KEYS_ADD: "/kms/add-key/",
KMS_KEYS_IMPORT: "/kms/import-key/",
/* Support */
TOOLS: "/support",
REGISTER_SUPPORT: "/support/register",
TOOLS_DIAGNOSTICS: "/support/diagnostics",
TOOLS_SPEEDTEST: "/support/speedtest",
CALL_HOME: "/support/call-home",
PROFILE: "/support/profile",
SUPPORT_INSPECT: "/support/inspect",
/** License **/
LICENSE: "/license",
/* Settings **/
@@ -389,15 +380,6 @@ export const IAM_PAGES_PERMISSIONS = {
IAM_SCOPES.ADMIN_SET_TIER, // display "add tier" button / shows add service tier page
IAM_SCOPES.ADMIN_LIST_TIERS, // display tiers list
],
[IAM_PAGES.TOOLS]: [
IAM_SCOPES.S3_LISTEN_NOTIFICATIONS, // displays watch notifications
IAM_SCOPES.S3_LISTEN_BUCKET_NOTIFICATIONS, // display watch notifications
IAM_SCOPES.ADMIN_GET_CONSOLE_LOG, // display minio console logs
IAM_SCOPES.ADMIN_SERVER_TRACE, // display minio trace
IAM_SCOPES.ADMIN_HEAL, // display heal
IAM_SCOPES.ADMIN_HEALTH_INFO, // display diagnostics / display speedtest / display audit log
IAM_SCOPES.ADMIN_SERVER_INFO, // display diagnostics
],
[IAM_PAGES.TOOLS_LOGS]: [IAM_SCOPES.ADMIN_GET_CONSOLE_LOG],
[IAM_PAGES.TOOLS_AUDITLOGS]: [IAM_SCOPES.ADMIN_HEALTH_INFO],
[IAM_PAGES.TOOLS_WATCH]: [
@@ -405,18 +387,6 @@ export const IAM_PAGES_PERMISSIONS = {
IAM_SCOPES.S3_LISTEN_BUCKET_NOTIFICATIONS, // display watch notifications
],
[IAM_PAGES.TOOLS_TRACE]: [IAM_SCOPES.ADMIN_SERVER_TRACE],
[IAM_PAGES.TOOLS_DIAGNOSTICS]: [
IAM_SCOPES.ADMIN_HEALTH_INFO,
IAM_SCOPES.ADMIN_SERVER_INFO,
],
[IAM_PAGES.TOOLS_SPEEDTEST]: [IAM_SCOPES.ADMIN_HEALTH_INFO],
[IAM_PAGES.REGISTER_SUPPORT]: [
IAM_SCOPES.ADMIN_SERVER_INFO,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
],
[IAM_PAGES.CALL_HOME]: [IAM_SCOPES.ADMIN_HEALTH_INFO],
[IAM_PAGES.PROFILE]: [IAM_SCOPES.ADMIN_HEALTH_INFO],
[IAM_PAGES.SUPPORT_INSPECT]: [IAM_SCOPES.ADMIN_HEALTH_INFO],
[IAM_PAGES.LICENSE]: [
IAM_SCOPES.ADMIN_SERVER_INFO,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,

View File

@@ -64,15 +64,6 @@ export const clearSession = () => {
deleteCookie("idp-refresh-token");
};
// timeFromDate gets time string from date input
export const timeFromDate = (d: Date) => {
let h = d.getHours() < 10 ? `0${d.getHours()}` : `${d.getHours()}`;
let m = d.getMinutes() < 10 ? `0${d.getMinutes()}` : `${d.getMinutes()}`;
let s = d.getSeconds() < 10 ? `0${d.getSeconds()}` : `${d.getSeconds()}`;
return `${h}:${m}:${s}:${d.getMilliseconds()}`;
};
// units to be used in a dropdown
export const k8sScalarUnitsExcluding = (exclude?: string[]) => {
return k8sUnits

View File

@@ -17,7 +17,6 @@
import React, { useEffect, useState } from "react";
import get from "lodash/get";
import { AddIcon, Box, Loader, Tag } from "mds";
import { Bucket } from "../../../Watch/types";
import { ErrorResponseHandler } from "../../../../../common/types";
import { IAM_SCOPES } from "../../../../../common/SecureComponent/permissions";
import { SecureComponent } from "../../../../../common/SecureComponent";
@@ -37,6 +36,15 @@ type BucketTagProps = {
bucketName: string;
};
interface Details {
tags: object;
}
interface Bucket {
details: Details;
name: string;
}
const BucketTags = ({ bucketName }: BucketTagProps) => {
const dispatch = useAppDispatch();

View File

@@ -1,140 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment, useEffect, useState } from "react";
import { DrivesIcon, Loader, SectionTitle, VersionIcon, Grid } from "mds";
import { api } from "api";
import { ServerProperties } from "api/consoleApi";
interface ITestWrapper {
title: any;
children: any;
}
const TestWrapper = ({ title, children }: ITestWrapper) => {
const [version, setVersion] = useState<string>("N/A");
const [totalNodes, setTotalNodes] = useState<number>(0);
const [totalDrives, setTotalDrives] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
if (loading) {
api.admin
.adminInfo({
defaultOnly: true,
})
.then((res) => {
const totalServers = res.data.servers?.length;
setTotalNodes(totalServers || 0);
if (res.data.servers && res.data.servers.length > 0) {
setVersion(res.data.servers[0].version || "N/A");
const totalServers = res.data.servers.reduce(
(prevTotal: number, currentElement: ServerProperties) => {
let c = currentElement.drives
? currentElement.drives.length
: 0;
return prevTotal + c;
},
0,
);
setTotalDrives(totalServers);
}
setLoading(false);
})
.catch(() => {
setLoading(false);
});
}
}, [loading]);
return (
<Grid item xs={12}>
<SectionTitle separator>{title}</SectionTitle>
<Grid item xs={12}>
<Grid
item
xs={12}
sx={{
padding: 0,
marginBottom: 25,
}}
>
<Grid
container
sx={{
padding: 25,
}}
>
{!loading ? (
<Fragment>
<Grid
item
xs={12}
md={4}
sx={{
fontSize: 18,
display: "flex",
alignItems: "center",
"& svg": {
marginRight: 10,
},
}}
>
<DrivesIcon /> <strong>{totalNodes}</strong>
&nbsp;nodes,&nbsp;
<strong>{totalDrives}</strong>&nbsp; drives
</Grid>
<Grid
item
xs={12}
md={4}
sx={{
fontSize: 12,
justifyContent: "center",
alignSelf: "center",
alignItems: "center",
display: "flex",
}}
>
<span
style={{
marginRight: 20,
}}
>
<VersionIcon />
</span>{" "}
MinIO VERSION&nbsp;<strong>{version}</strong>
</Grid>
</Fragment>
) : (
<Fragment>
<Grid item xs={12} sx={{ textAlign: "center" }}>
<Loader style={{ width: 25, height: 25 }} />
</Grid>
</Fragment>
)}
</Grid>
</Grid>
{children}
</Grid>
</Grid>
);
};
export default TestWrapper;

View File

@@ -49,10 +49,6 @@ import MenuWrapper from "./Menu/MenuWrapper";
import LoadingComponent from "../../common/LoadingComponent";
import ComponentsScreen from "./Common/ComponentsScreen";
const Trace = React.lazy(() => import("./Trace/Trace"));
const Watch = React.lazy(() => import("./Watch/Watch"));
const HealthInfo = React.lazy(() => import("./HealthInfo/HealthInfo"));
const EventDestinations = React.lazy(
() => import("./EventDestinations/EventDestinations"),
);
@@ -79,11 +75,8 @@ const LogsSearchMain = React.lazy(
);
const GroupsDetails = React.lazy(() => import("./Groups/GroupsDetails"));
const Tools = React.lazy(() => import("./Tools/Tools"));
const IconsScreen = React.lazy(() => import("./Common/IconsScreen"));
const Speedtest = React.lazy(() => import("./Speedtest/Speedtest"));
const ObjectManager = React.lazy(
() => import("./Common/ObjectManager/ObjectManager"),
);
@@ -278,15 +271,6 @@ const Console = () => {
);
},
},
{
component: Watch,
path: IAM_PAGES.TOOLS_WATCH,
},
{
component: Speedtest,
path: IAM_PAGES.TOOLS_SPEEDTEST,
},
{
component: Users,
path: IAM_PAGES.USERS,
@@ -336,14 +320,6 @@ const Console = () => {
component: IDPOpenIDConfigurationDetails,
path: IAM_PAGES.IDP_OPENID_CONFIGURATIONS_VIEW,
},
{
component: Trace,
path: IAM_PAGES.TOOLS_TRACE,
},
{
component: HealthInfo,
path: IAM_PAGES.TOOLS_DIAGNOSTICS,
},
{
component: ErrorLogs,
path: IAM_PAGES.TOOLS_LOGS,
@@ -352,10 +328,6 @@ const Console = () => {
component: LogsSearchMain,
path: IAM_PAGES.TOOLS_AUDITLOGS,
},
{
component: Tools,
path: IAM_PAGES.TOOLS,
},
{
component: ConfigurationOptions,
path: IAM_PAGES.SETTINGS,

View File

@@ -1,342 +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 } from "react-router-dom";
import { Box, Button, Grid, HelpBox, InfoIcon, Loader, PageLayout } from "mds";
import {
DiagStatError,
DiagStatInProgress,
DiagStatSuccess,
HealthInfoMessage,
ReportMessage,
} from "./types";
import { AppState, useAppDispatch } from "../../../store";
import {
WSCloseAbnormalClosure,
WSCloseInternalServerErr,
WSClosePolicyViolation,
wsProtocol,
} from "../../../utils/wsUtils";
import { setHelpName, setServerDiagStat } from "../../../systemSlice";
import {
healthInfoMessageReceived,
healthInfoResetMessage,
} from "./healthInfoSlice";
import { registeredCluster } from "../../../config";
import TestWrapper from "../Common/TestWrapper/TestWrapper";
import RegisterCluster from "../Support/RegisterCluster";
import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper";
import HelpMenu from "../HelpMenu";
const HealthInfo = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const message = useSelector((state: AppState) => state.healthInfo.message);
const serverDiagnosticStatus = useSelector(
(state: AppState) => state.system.serverDiagnosticStatus,
);
const [startDiagnostic, setStartDiagnostic] = useState(false);
const [downloadDisabled, setDownloadDisabled] = useState(true);
const [localMessage, setMessage] = useState<string>("");
const [buttonStartText, setButtonStartText] = useState<string>(
"Start Health Report",
);
const [title, setTitle] = useState<string>("Health Report");
const [diagFileContent, setDiagFileContent] = useState<string>("");
const [subnetResponse, setSubnetResponse] = useState<string>("");
const clusterRegistered = registeredCluster();
const download = () => {
let element = document.createElement("a");
element.setAttribute(
"href",
`data:application/gzip;base64,${diagFileContent}`,
);
element.setAttribute("download", "diagnostic.json.gz");
element.style.display = "none";
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
};
useEffect(() => {
if (serverDiagnosticStatus === DiagStatInProgress) {
setTitle("Health Report in progress...");
setMessage(
"Health Report started. Please do not refresh page during diagnosis.",
);
return;
}
if (serverDiagnosticStatus === DiagStatSuccess) {
setTitle("Health Report complete");
setMessage("Health Report file is ready to be downloaded.");
setButtonStartText("Start Health Report");
return;
}
if (serverDiagnosticStatus === DiagStatError) {
setTitle("Error");
setMessage("An error occurred while getting the Health Report file.");
setButtonStartText("Retry Health Report");
return;
}
}, [serverDiagnosticStatus, startDiagnostic]);
useEffect(() => {
if (
serverDiagnosticStatus === DiagStatSuccess &&
message !== ({} as HealthInfoMessage)
) {
// Allow download of diagnostics file only when
// it succeded fetching all the results and info is not empty.
setDownloadDisabled(false);
}
if (serverDiagnosticStatus === DiagStatInProgress) {
// Disable Start Health Report and Disable Download buttons
// if a Diagnosis is in progress.
setDownloadDisabled(true);
}
setStartDiagnostic(false);
}, [serverDiagnosticStatus, message]);
useEffect(() => {
if (startDiagnostic) {
dispatch(healthInfoResetMessage());
setDiagFileContent("");
const url = new URL(window.location.toString());
const isDev = process.env.NODE_ENV === "development";
const port = isDev ? "9090" : url.port;
const wsProt = wsProtocol(url.protocol);
// check if we are using base path, if not this always is `/`
const baseLocation = new URL(document.baseURI);
const baseUrl = baseLocation.pathname;
const socket = new WebSocket(
`${wsProt}://${url.hostname}:${port}${baseUrl}ws/health-info?deadline=1h`,
);
let interval: any | null = null;
if (socket !== null) {
socket.onopen = () => {
console.log("WebSocket Client Connected");
socket.send("ok");
interval = setInterval(() => {
socket.send("ok");
}, 10 * 1000);
setMessage(
"Health Report started. Please do not refresh page during diagnosis.",
);
dispatch(setServerDiagStat(DiagStatInProgress));
};
socket.onmessage = (message: MessageEvent) => {
let m: ReportMessage = JSON.parse(message.data.toString());
if (m.serverHealthInfo) {
dispatch(healthInfoMessageReceived(m.serverHealthInfo));
}
if (m.encoded !== "") {
setDiagFileContent(m.encoded);
}
if (m.subnetResponse) {
setSubnetResponse(m.subnetResponse);
}
};
socket.onerror = (error) => {
console.error("error closing websocket:", error);
socket.close(1000);
clearInterval(interval);
dispatch(setServerDiagStat(DiagStatError));
};
socket.onclose = (event: CloseEvent) => {
clearInterval(interval);
if (
event.code === WSCloseInternalServerErr ||
event.code === WSClosePolicyViolation ||
event.code === WSCloseAbnormalClosure
) {
// handle close with error
console.log("connection closed by server with code:", event.code);
setMessage(
"An error occurred while getting the Health Report file.",
);
dispatch(setServerDiagStat(DiagStatError));
} else {
console.log("connection closed by server");
setMessage("Health Report file is ready to be downloaded.");
dispatch(setServerDiagStat(DiagStatSuccess));
}
};
}
} else {
// reset start status
setStartDiagnostic(false);
}
}, [startDiagnostic, dispatch]);
const startDiagnosticAction = () => {
if (!clusterRegistered) {
navigate("/support/register");
return;
}
setStartDiagnostic(true);
};
useEffect(() => {
dispatch(setHelpName("health_info"));
}, [dispatch]);
return (
<Fragment>
<PageHeaderWrapper label="Health" actions={<HelpMenu />} />
<PageLayout>
{!clusterRegistered && <RegisterCluster compactMode />}
<Box withBorders>
<TestWrapper title={title}>
<Grid
container
sx={{
justifyContent: "flex-start",
gap: 20,
}}
>
<Grid
key="start-download"
item
xs={12}
sx={{
textAlign: "center",
marginBottom: 25,
}}
>
<h2>{localMessage}</h2>
<Box
sx={{
textAlign: "center",
marginBottom: 25,
}}
>
{" "}
{subnetResponse !== "" &&
!subnetResponse.toLowerCase().includes("error") && (
<Grid item xs={12}>
<strong>
Health report uploaded to SUBNET successfully!
</strong>
&nbsp;{" "}
<strong>
See the results on your{" "}
<a href={subnetResponse}>SUBNET Dashboard</a>{" "}
</strong>
</Grid>
)}
{(subnetResponse === "" ||
subnetResponse.toLowerCase().includes("error")) &&
serverDiagnosticStatus === DiagStatSuccess && (
<Grid item xs={12}>
<strong>
Something went wrong uploading your Health report to
SUBNET.
</strong>
&nbsp;{" "}
<strong>
Log into your{" "}
<a href="https://subnet.min.io">SUBNET Account</a> to
manually upload your Health report.
</strong>
</Grid>
)}
</Box>
{serverDiagnosticStatus === DiagStatInProgress ? (
<Box
sx={{
paddingTop: 8,
paddingLeft: 40,
}}
>
<Loader style={{ width: 25, height: 25 }} />
</Box>
) : (
<Fragment>
<Box
sx={{
display: "flex",
gap: 10,
alignItems: "center",
justifyContent: "center",
}}
>
<Box>
{serverDiagnosticStatus !== DiagStatError &&
!downloadDisabled && (
<Button
id={"download"}
type="submit"
variant="callAction"
onClick={() => download()}
disabled={downloadDisabled}
label={"Download"}
/>
)}
</Box>
<Box>
<Button
id="start-new-diagnostic"
type="submit"
variant={
!clusterRegistered ? "regular" : "callAction"
}
disabled={startDiagnostic || !clusterRegistered}
onClick={startDiagnosticAction}
label={buttonStartText}
/>
</Box>
</Box>
</Fragment>
)}
</Grid>
</Grid>
</TestWrapper>
</Box>
{!startDiagnostic && clusterRegistered && (
<Fragment>
<br />
<HelpBox
title={
"Cluster Health Report will be uploaded to SUBNET, and is viewable from your SUBNET Diagnostics dashboard."
}
iconComponent={<InfoIcon />}
help={
"If the Health report cannot be generated at this time, please wait a moment and try again."
}
/>
</Fragment>
)}
</PageLayout>
</Fragment>
);
};
export default HealthInfo;

View File

@@ -1,46 +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 { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { HealthInfoMessage } from "./types";
interface HealthInfoState {
message: HealthInfoMessage;
}
const initialState: HealthInfoState = {
message: {} as HealthInfoMessage,
};
const healthInfoSlice = createSlice({
name: "trace",
initialState,
reducers: {
healthInfoMessageReceived: (
state,
action: PayloadAction<HealthInfoMessage>,
) => {
state.message = action.payload;
},
healthInfoResetMessage: (state) => {
state.message = {} as HealthInfoMessage;
},
},
});
export const { healthInfoMessageReceived, healthInfoResetMessage } =
healthInfoSlice.actions;
export default healthInfoSlice.reducer;

View File

@@ -1,528 +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 DiagStatError = "error";
export const DiagStatSuccess = "success";
export const DiagStatInProgress = "inProgress";
export interface HealthInfoMessage {
timestamp: string;
error: string;
perf: perfInfo;
minio: minioHealthInfo;
sys: sysHealthInfo;
}
export interface ReportMessage {
encoded: string;
serverHealthInfo: HealthInfoMessage;
subnetResponse: string;
}
interface perfInfo {
drives: serverDrivesInfo[];
net: serverNetHealthInfo[];
net_parallel: serverNetHealthInfo;
error: string;
}
interface serverDrivesInfo {
addr: string;
serial: drivePerfInfo[];
parallel: drivePerfInfo[];
error: string;
}
interface drivePerfInfo {
endpoint: string;
latency: diskLatency;
throughput: diskThroughput;
error: string;
}
interface diskLatency {
avg_secs: number;
percentile50_secs: number;
percentile90_secs: number;
percentile99_secs: number;
min_secs: number;
max_secs: number;
}
interface diskThroughput {
avg_bytes_per_sec: number;
percentile50_bytes_per_sec: number;
percentile90_bytes_per_sec: number;
percentile99_bytes_per_sec: number;
min_bytes_per_sec: number;
max_bytes_per_sec: number;
}
interface serverNetHealthInfo {
addr: string;
net: netPerfInfo[];
error: string;
}
interface netPerfInfo {
remote: string;
latency: netLatency;
throughput: netThroughput;
error: string;
}
interface netLatency {
avg_secs: number;
percentile50_secs: number;
percentile90_secs: number;
percentile99_secs: number;
min_secs: number;
max_secs: number;
}
interface netThroughput {
avg_bytes_per_sec: number;
percentile50_bytes_per_sec: number;
percentile90_bytes_per_sec: number;
percentile99_bytes_per_sec: number;
min_bytes_per_sec: number;
max_bytes_per_sec: number;
}
interface minioHealthInfo {
info: infoMessage;
config: any;
error: string;
}
interface infoMessage {
mode: string;
domain: string[];
region: string;
sqsARN: string[];
deploymentID: string;
buckets: buckets;
objects: objects;
usage: usage;
services: services;
backend: any;
servers: serverProperties[];
}
interface buckets {
count: number;
}
interface objects {
count: number;
}
interface usage {
size: number;
}
interface services {
vault: vault;
ldap: ldap;
logger: Map<string, status[]>[];
audit: Map<string, status[]>[];
notifications: Map<string, Map<string, status[]>[]>;
}
interface vault {
status: string;
encrypt: string;
decrypt: string;
}
interface ldap {
status: string;
}
interface status {
status: string;
}
interface serverProperties {
state: string;
endpoint: string;
uptime: number;
version: string;
commitID: string;
network: Map<string, string>;
drives: disk[];
}
interface disk {
endpoint: string;
rootDisk: boolean;
path: string;
healing: boolean;
state: string;
uuid: string;
model: string;
totalspace: number;
usedspace: number;
availspace: number;
readthroughput: number;
writethroughput: number;
readlatency: number;
writelatency: number;
utilization: number;
}
interface sysHealthInfo {
cpus: serverCpuInfo[];
drives: serverDiskHwInfo[];
osinfos: serverOsInfo[];
meminfos: serverMemInfo[];
procinfos: serverProcInfo[];
error: string;
}
interface serverCpuInfo {
addr: string;
cpu: cpuInfoStat[];
time: cpuTimeStat[];
error: string;
}
interface cpuInfoStat {
cpu: number;
vendorId: string;
family: string;
model: string;
stepping: number;
physicalId: string;
coreId: string;
cores: number;
modelName: string;
mhz: number;
cacheSize: number;
flags: string[];
microcode: string;
}
interface cpuTimeStat {
cpu: string;
user: number;
system: number;
idle: number;
nice: number;
iowait: number;
irq: number;
softirq: number;
steal: number;
guest: number;
guestNice: number;
}
interface serverDiskHwInfo {
addr: string;
usages: diskUsageStat[];
partitions: partitionStat[];
counters: Map<string, diskIOCountersStat>;
error: string;
}
interface diskUsageStat {
path: string;
fstype: string;
total: number;
free: number;
used: number;
usedPercent: number;
inodesTotal: number;
inodesUsed: number;
inodesFree: number;
inodesUsedPercent: number;
}
interface partitionStat {
device: string;
mountpoint: string;
fstype: string;
opts: string;
smartInfo: smartInfo;
}
interface smartInfo {
device: string;
scsi: scsiInfo;
nvme: nvmeInfo;
ata: ataInfo;
error: string;
}
interface scsiInfo {
scsiCapacityBytes: number;
scsiModeSenseBuf: string;
scsirespLen: number;
scsiBdLen: number;
scsiOffset: number;
sciRpm: number;
}
interface nvmeInfo {
serialNum: string;
vendorId: string;
firmwareVersion: string;
modelNum: string;
spareAvailable: string;
spareThreshold: string;
temperature: string;
criticalWarning: string;
maxDataTransferPages: number;
controllerBusyTime: number;
powerOnHours: number;
powerCycles: number;
unsafeShutdowns: number;
mediaAndDataIntgerityErrors: number;
dataUnitsReadBytes: number;
dataUnitsWrittenBytes: number;
hostReadCommands: number;
hostWriteCommands: number;
}
interface ataInfo {
scsiLuWWNDeviceID: string;
serialNum: string;
modelNum: string;
firmwareRevision: string;
RotationRate: string;
MajorVersion: string;
MinorVersion: string;
smartSupportAvailable: boolean;
smartSupportEnabled: boolean;
smartErrorLog: string;
transport: string;
}
interface diskIOCountersStat {
readCount: number;
mergedReadCount: number;
DriteCount: number;
mergedWriteCount: number;
readBytes: number;
writeBytes: number;
readTime: number;
writeTime: number;
iopsInProgress: number;
ioTime: number;
weightedIO: number;
name: string;
serialNumber: string;
label: string;
}
interface serverOsInfo {
addr: string;
info: infoStat;
sensors: temperatureStat[];
users: userStat[];
error: string;
}
interface infoStat {
hostname: string;
uptime: number;
bootTime: number;
procs: number;
os: string;
platform: string;
platformFamily: string;
platformVersion: string;
kernelVersion: string;
kernelArch: string;
virtualizationSystem: string;
virtualizationRole: string;
hostid: string;
}
interface temperatureStat {
sensorKey: string;
sensorTemperature: number;
}
interface userStat {
user: string;
terminal: string;
host: string;
started: number;
}
interface serverMemInfo {
addr: string;
swap: swapMemoryStat;
virtualmem: virtualMemoryStat;
error: string;
}
interface swapMemoryStat {
total: number;
used: number;
free: number;
usedPercent: number;
sin: number;
sout: number;
pgin: number;
pgout: number;
pgfault: number;
pgmajfault: number;
}
interface virtualMemoryStat {
total: number;
available: number;
used: number;
usedPercent: number;
free: number;
active: number;
inactive: number;
wired: number;
laundry: number;
buffers: number;
cached: number;
writeback: number;
dirty: number;
writebacktmp: number;
shared: number;
slab: number;
sreclaimable: number;
sunreclaim: number;
pagetables: number;
swapcached: number;
commitlimit: number;
committedas: number;
hightotal: number;
highfree: number;
lowtotal: number;
lowfree: number;
swaptotal: number;
swapfree: number;
mapped: number;
vmalloctotal: number;
vmallocused: number;
vmallocchunk: number;
hugepagestotal: number;
hugepagesfree: number;
hugepagesize: number;
}
interface serverProcInfo {
addr: string;
processes: sysProcess[];
error: string;
}
interface sysProcess {
pid: number;
background: boolean;
cpupercent: number;
children: number[];
cmd: string;
connections: nethwConnectionStat[];
createtime: number;
cwd: string;
exe: string;
gids: number[];
iocounters: processIOCountersStat;
isrunning: boolean;
meminfo: memoryInfoStat;
memmaps: any[];
mempercent: number;
name: string;
netiocounters: nethwIOCounterStat[];
nice: number;
numctxswitches: processNmCtxSwitchesStat;
numfds: number;
numthreads: number;
pagefaults: processPageFaultsStat;
parent: number;
ppid: number;
rlimit: processRLimitStat[];
status: string;
tgid: number;
cputimes: cpuTimeStat;
uids: number[];
username: string;
}
interface nethwConnectionStat {
fd: number;
family: number;
type: number;
localaddr: netAddr;
remoteaddr: netAddr;
status: string;
uids: number[];
pid: number;
}
interface netAddr {
ip: string;
port: number;
}
interface processIOCountersStat {
readCount: number;
writeCount: number;
readBytes: number;
writeBytes: number;
}
interface memoryInfoStat {
rss: number;
vms: number;
hwm: number;
data: number;
stack: number;
locked: number;
swap: number;
}
interface nethwIOCounterStat {
name: string;
bytesSent: number;
bytesRecv: number;
packetsSent: number;
packetsRecv: number;
errin: number;
errout: number;
dropin: number;
dropout: number;
fifoin: number;
fifoout: number;
}
interface processNmCtxSwitchesStat {
voluntary: number;
involuntary: number;
}
interface processPageFaultsStat {
minorFaults: number;
majorFaults: number;
childMinorFaults: number;
childMajorFaults: number;
}
interface processRLimitStat {
resource: number;
soft: number;
hard: number;
used: number;
}

View File

@@ -15,17 +15,13 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment, useCallback, useEffect, useState } from "react";
import { ArrowIcon, Button, PageLayout, ProgressBar, Grid } from "mds";
import { PageLayout, ProgressBar, Grid } from "mds";
import { SubnetInfo } from "./types";
import api from "../../../common/api";
import { IAM_PAGES } from "../../../common/SecureComponent/permissions";
import LicensePlans from "./LicensePlans";
import { useNavigate } from "react-router-dom";
import RegistrationStatusBanner from "../Support/RegistrationStatusBanner";
import withSuspense from "../Common/Components/withSuspense";
import { getLicenseConsent } from "./utils";
import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper";
import HelpMenu from "../HelpMenu";
import { setHelpName } from "../../../systemSlice";
import { useAppDispatch } from "../../../store";
@@ -34,7 +30,6 @@ const LicenseConsentModal = withSuspense(
);
const License = () => {
const navigate = useNavigate();
const [activateProductModal, setActivateProductModal] =
useState<boolean>(false);
@@ -126,38 +121,9 @@ const License = () => {
return (
<Fragment>
<PageHeaderWrapper
label="MinIO License and Support Plan"
actions={
<Fragment>
{!isRegistered && (
<Button
id={"login-with-subnet"}
onClick={() => navigate(IAM_PAGES.REGISTER_SUPPORT)}
style={{
fontSize: "14px",
display: "flex",
alignItems: "center",
textDecoration: "none",
}}
icon={<ArrowIcon />}
variant={"callAction"}
>
Register your cluster
</Button>
)}
<HelpMenu />
</Fragment>
}
/>
<PageHeaderWrapper label="MinIO License and Support Plan" />
<PageLayout>
<Grid item xs={12}>
{isRegistered && (
<RegistrationStatusBanner email={licenseInfo?.email} />
)}
</Grid>
<LicensePlans
activateProductModal={activateProductModal}
closeModalAndFetchLicenseInfo={closeModalAndFetchLicenseInfo}

View File

@@ -22,42 +22,3 @@ export interface SubnetInfo {
storage_capacity: number;
organization: string;
}
export interface SubnetLoginRequest {
username?: string;
password?: string;
apiKey?: string;
proxy?: string;
}
export interface SubnetRegisterRequest {
token: string;
account_id: string;
}
export interface SubnetOrganization {
userId: number;
accountId: number;
subscriptionStatus: string;
isAccountOwner: boolean;
shortName: string;
company: string;
}
export interface SubnetLoginResponse {
registered: boolean;
mfa_token: string;
access_token: string;
organizations: SubnetOrganization[];
}
export interface SubnetLoginWithMFARequest {
username: string;
otp: string;
mfa_token: string;
proxy?: string;
}
export interface SubnetRegTokenResponse {
regToken: string;
}

View File

@@ -1,455 +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 {
Button,
ComputerLineIcon,
DownloadIcon,
DownloadStatIcon,
JSONIcon,
StorageIcon,
UploadStatIcon,
VersionIcon,
Grid,
Box,
} from "mds";
import { IndvServerMetric, SpeedTestResponse, STServer } from "./types";
import { calculateBytes, prettyNumber } from "../../../common/utils";
import { Area, AreaChart, CartesianGrid, ResponsiveContainer } from "recharts";
import { cleanMetrics } from "./utils";
import CodeMirrorWrapper from "../Common/FormComponents/CodeMirrorWrapper/CodeMirrorWrapper";
import SpeedTestUnit from "./SpeedTestUnit";
import styled from "styled-components";
const STResultsContainer = styled.div(({ theme }) => ({
"& .actionButtons": {
textAlign: "right",
},
"& .descriptorLabel": {
fontWeight: "bold",
fontSize: 14,
},
"& .resultsContainer": {
backgroundColor: get(theme, "boxBackground", "#FBFAFA"),
borderTop: `${get(theme, "borderColor", "#E2E2E2")} 1px solid`,
marginTop: 30,
padding: 25,
},
"& .resultsIcon": {
display: "flex",
alignItems: "center",
"& svg": {
fill: get(theme, `screenTitle.iconColor`, "#07193E"),
},
},
"& .detailedItem": {
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
},
"& .detailedVersion": {
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
},
"& .serversTable": {
width: "100%",
marginTop: 15,
"& thead > tr > th": {
textAlign: "left",
padding: 15,
fontSize: 14,
fontWeight: "bold",
},
"& tbody > tr": {
"&:last-of-type": {
"& > td": {
borderBottom: `${get(theme, "borderColor", "#E2E2E2")} 1px solid`,
},
},
"& > td": {
borderTop: `${get(theme, "borderColor", "#E2E2E2")} 1px solid`,
padding: 15,
fontSize: 14,
"&:first-of-type": {
borderLeft: `${get(theme, "borderColor", "#E2E2E2")} 1px solid`,
},
"&:last-of-type": {
borderRight: `${get(theme, "borderColor", "#E2E2E2")} 1px solid`,
},
},
},
},
"& .serverIcon": {
width: 55,
},
"& .serverValue": {
width: 140,
},
"& .serverHost": {
maxWidth: 540,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
},
"& .tableOverflow": {
overflowX: "auto",
paddingBottom: 15,
},
"& .objectGeneral": {
marginTop: 15,
},
"& .download": {
"& .min-icon": {
width: 35,
height: 35,
color: get(theme, "signalColors.good", "#4CCB92"),
},
},
"& .upload": {
"& .min-icon": {
width: 35,
height: 35,
color: get(theme, "signalColors.info", "#2781B0"),
},
},
"& .versionIcon": {
color: get(theme, `screenTitle.iconColor`, "#07193E"),
marginRight: 20,
},
}));
interface ISTResults {
results: SpeedTestResponse[];
start: boolean;
}
const STResults = ({ results, start }: ISTResults) => {
const [jsonView, setJsonView] = useState<boolean>(false);
const finalRes = results[results.length - 1] || [];
const getServers: STServer[] = get(finalRes, "GETStats.servers", []) || [];
const putServers: STServer[] = get(finalRes, "PUTStats.servers", []) || [];
const getThroughput = get(finalRes, "GETStats.throughputPerSec", 0);
const getObjects = get(finalRes, "GETStats.objectsPerSec", 0);
const putThroughput = get(finalRes, "PUTStats.throughputPerSec", 0);
const putObjects = get(finalRes, "PUTStats.objectsPerSec", 0);
let statJoin: IndvServerMetric[] = [];
getServers.forEach((item) => {
const hostName = item.endpoint;
const putMetric = putServers.find((item) => item.endpoint === hostName);
let itemJoin: IndvServerMetric = {
getUnit: "-",
getValue: "N/A",
host: item.endpoint,
putUnit: "-",
putValue: "N/A",
};
if (item.err && item.err !== "") {
itemJoin.getError = item.err;
itemJoin.getUnit = "-";
itemJoin.getValue = "N/A";
} else {
const niceGet = calculateBytes(item.throughputPerSec.toString());
itemJoin.getUnit = niceGet.unit;
itemJoin.getValue = niceGet.total.toString();
}
if (putMetric) {
if (putMetric.err && putMetric.err !== "") {
itemJoin.putError = putMetric.err;
itemJoin.putUnit = "-";
itemJoin.putValue = "N/A";
} else {
const nicePut = calculateBytes(putMetric.throughputPerSec.toString());
itemJoin.putUnit = nicePut.unit;
itemJoin.putValue = nicePut.total.toString();
}
}
statJoin.push(itemJoin);
});
const downloadResults = () => {
const date = new Date();
let element = document.createElement("a");
element.setAttribute(
"href",
"data:text/plain;charset=utf-8," + JSON.stringify(finalRes),
);
element.setAttribute(
"download",
`speedtest_results-${date.toISOString()}.log`,
);
element.style.display = "none";
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
};
const toggleJSONView = () => {
setJsonView(!jsonView);
};
const finalResJSON = finalRes ? JSON.stringify(finalRes, null, 4) : "";
const clnMetrics = cleanMetrics(results);
return (
<STResultsContainer>
<Grid container className={"objectGeneral"}>
<Grid item xs={12} md={6} lg={6}>
<Grid container className={"objectGeneral"}>
<Grid item xs={12} md={6} lg={6}>
<SpeedTestUnit
icon={
<div className={"download"}>
<DownloadStatIcon />
</div>
}
title={"GET"}
throughput={`${getThroughput}`}
objects={getObjects}
/>
</Grid>
<Grid item xs={12} md={6} lg={6}>
<SpeedTestUnit
icon={
<div className={"upload"}>
<UploadStatIcon />
</div>
}
title={"PUT"}
throughput={`${putThroughput}`}
objects={putObjects}
/>
</Grid>
</Grid>
</Grid>
<Grid item xs={12} md={6} lg={6}>
<ResponsiveContainer width="99%">
<AreaChart data={clnMetrics}>
<defs>
<linearGradient id="colorPut" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#2781B0" stopOpacity={0.9} />
<stop offset="95%" stopColor="#fff" stopOpacity={0} />
</linearGradient>
<linearGradient id="colorGet" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#4CCB92" stopOpacity={0.9} />
<stop offset="95%" stopColor="#fff" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid
strokeDasharray={"0 0"}
strokeWidth={1}
strokeOpacity={0.5}
stroke={"#F1F1F1"}
vertical={false}
/>
<Area
type="monotone"
dataKey={"get"}
stroke={"#4CCB92"}
fill={"url(#colorGet)"}
fillOpacity={0.3}
strokeWidth={2}
dot={false}
/>
<Area
type="monotone"
dataKey={"put"}
stroke={"#2781B0"}
fill={"url(#colorPut)"}
fillOpacity={0.3}
strokeWidth={2}
dot={false}
/>
</AreaChart>
</ResponsiveContainer>
</Grid>
</Grid>
<br />
{clnMetrics.length > 1 && (
<Fragment>
<Grid container>
<Grid item xs={12} md={6} className={"descriptorLabel"}>
{start ? (
<Fragment>Preliminar Results:</Fragment>
) : (
<Fragment>
{jsonView ? "JSON Results:" : "Detailed Results:"}
</Fragment>
)}
</Grid>
<Grid
item
xs={12}
md={6}
sx={{ display: "flex", justifyContent: "right", gap: 8 }}
>
{!start && (
<Fragment>
<Button
id={"download-results"}
aria-label="Download Results"
onClick={downloadResults}
icon={<DownloadIcon />}
/>
&nbsp;
<Button
id={"toggle-json"}
aria-label="Toogle JSON"
onClick={toggleJSONView}
icon={<JSONIcon />}
/>
</Fragment>
)}
</Grid>
</Grid>
<Box withBorders useBackground sx={{ marginTop: 15 }}>
<Grid container>
{jsonView ? (
<Fragment>
<CodeMirrorWrapper value={finalResJSON} onChange={() => {}} />
</Fragment>
) : (
<Fragment>
<Grid
item
xs={12}
sm={12}
md={1}
lg={1}
className={"resultsIcon"}
>
<ComputerLineIcon width={45} />
</Grid>
<Grid
item
xs={12}
sm={6}
md={3}
lg={2}
className={"detailedItem"}
>
Nodes:&nbsp;<strong>{finalRes.servers}</strong>
</Grid>
<Grid
item
xs={12}
sm={6}
md={3}
lg={2}
className={"detailedItem"}
>
Drives:&nbsp;<strong>{finalRes.disks}</strong>
</Grid>
<Grid
item
xs={12}
sm={6}
md={3}
lg={2}
className={"detailedItem"}
>
Concurrent:&nbsp;<strong>{finalRes.concurrent}</strong>
</Grid>
<Grid
item
xs={12}
sm={12}
md={12}
lg={5}
className={"detailedVersion"}
>
<span className={"versionIcon"}>
<VersionIcon />
</span>{" "}
MinIO VERSION&nbsp;<strong>{finalRes.version}</strong>
</Grid>
<Grid item xs={12} className={"tableOverflow"}>
<table
className={"serversTable"}
cellSpacing={0}
cellPadding={0}
>
<thead>
<tr>
<th colSpan={2}>Servers</th>
<th>GET</th>
<th>PUT</th>
</tr>
</thead>
<tbody>
{statJoin.map((stats, index) => (
<tr key={`storage-${index.toString()}`}>
<td className={"serverIcon"}>
<StorageIcon />
</td>
<td className={"serverHost"}>{stats.host}</td>
{stats.getError && stats.getError !== "" ? (
<td>{stats.getError}</td>
) : (
<Fragment>
<td className={"serverValue"}>
{prettyNumber(parseFloat(stats.getValue))}
&nbsp;
{stats.getUnit}/s.
</td>
</Fragment>
)}
{stats.putError && stats.putError !== "" ? (
<td>{stats.putError}</td>
) : (
<Fragment>
<td className={"serverValue"}>
{prettyNumber(parseFloat(stats.putValue))}
&nbsp;
{stats.putUnit}/s.
</td>
</Fragment>
)}
</tr>
))}
</tbody>
</table>
</Grid>
</Fragment>
)}
</Grid>
</Box>
</Fragment>
)}
</STResultsContainer>
);
};
export default STResults;

View File

@@ -1,99 +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 styled from "styled-components";
import get from "lodash/get";
import { calculateBytes } from "../../../common/utils";
const SpeedTestUnitBase = styled.table(({ theme }) => ({
"& .objectGeneralTitle": {
lineHeight: 1,
fontSize: 50,
color: get(theme, "mutedText", "#87888d"),
},
"& .generalUnit": {
color: get(theme, "fontColor", "#000"),
fontSize: 12,
fontWeight: "bold",
},
"& .testUnitRes": {
fontSize: 60,
color: get(theme, "signalColors.main", "#07193E"),
fontWeight: "bold",
textAlign: "right",
},
"& .metricValContainer": {
lineHeight: 1,
verticalAlign: "bottom",
},
"& .objectsUnitRes": {
fontSize: 22,
marginTop: 6,
color: get(theme, "mutedText", "#87888d"),
fontWeight: "bold",
textAlign: "right",
},
"& .objectsUnit": {
color: get(theme, "mutedText", "#87888d"),
fontSize: 16,
fontWeight: "bold",
},
"& .iconTd": {
verticalAlign: "bottom",
},
}));
const SpeedTestUnit = ({
title,
icon,
throughput,
objects,
}: {
title: any;
icon: any;
throughput: string;
objects: number;
}) => {
const avg = calculateBytes(throughput);
let total = "0";
let unit = "";
if (avg.total !== 0) {
total = avg.total.toString();
unit = `${avg.unit}/s`;
}
return (
<SpeedTestUnitBase>
<tr>
<td className={"objectGeneralTitle"}>{title}</td>
<td className={"iconTd"}>{icon}</td>
</tr>
<tr>
<td className={`metricValContainer testUnitRes`}>{total}</td>
<td className={`metricValContainer generalUnit`}>{unit}</td>
</tr>
<tr>
<td className={`metricValContainer objectsUnitRes`}>{objects}</td>
<td className={`metricValContainer objectsUnit`}>
{objects !== 0 && "Objs/S"}
</td>
</tr>
</SpeedTestUnitBase>
);
};
export default SpeedTestUnit;

View File

@@ -1,331 +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 } from "react-router-dom";
import {
Box,
Button,
Grid,
HelpBox,
InputBox,
Loader,
PageLayout,
SpeedtestIcon,
WarnIcon,
} from "mds";
import { DateTime } from "luxon";
import STResults from "./STResults";
import ProgressBarWrapper from "../Common/ProgressBarWrapper/ProgressBarWrapper";
import InputUnitMenu from "../Common/FormComponents/InputUnitMenu/InputUnitMenu";
import DistributedOnly from "../Common/DistributedOnly/DistributedOnly";
import RegisterCluster from "../Support/RegisterCluster";
import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper";
import HelpMenu from "../HelpMenu";
import { SecureComponent } from "../../../common/SecureComponent";
import { selDistSet, setHelpName } from "../../../systemSlice";
import { registeredCluster } from "../../../config";
import { useAppDispatch } from "../../../store";
import { wsProtocol } from "../../../utils/wsUtils";
import { SpeedTestResponse } from "./types";
import {
CONSOLE_UI_RESOURCE,
IAM_SCOPES,
} from "../../../common/SecureComponent/permissions";
const Speedtest = () => {
const distributedSetup = useSelector(selDistSet);
const navigate = useNavigate();
const [start, setStart] = useState<boolean>(false);
const [currStatus, setCurrStatus] = useState<SpeedTestResponse[] | null>(
null,
);
const [size, setSize] = useState<string>("64");
const [sizeUnit, setSizeUnit] = useState<string>("MB");
const [duration, setDuration] = useState<string>("10");
const [topDate, setTopDate] = useState<number>(0);
const [currentValue, setCurrentValue] = useState<number>(0);
const [totalSeconds, setTotalSeconds] = useState<number>(0);
const [speedometerValue, setSpeedometerValue] = useState<number>(0);
const clusterRegistered = registeredCluster();
useEffect(() => {
// begin watch if bucketName in bucketList and start pressed
if (start) {
const url = new URL(window.location.toString());
const isDev = process.env.NODE_ENV === "development";
const port = isDev ? "9090" : url.port;
// check if we are using base path, if not this always is `/`
const baseLocation = new URL(document.baseURI);
const baseUrl = baseLocation.pathname;
const wsProt = wsProtocol(url.protocol);
const socket = new WebSocket(
`${wsProt}://${url.hostname}:${port}${baseUrl}ws/speedtest?&size=${size}${sizeUnit}&duration=${duration}s`,
);
const baseDate = DateTime.now();
const currentTime = baseDate.toUnixInteger() / 1000;
const incrementDate =
baseDate.plus({ seconds: parseInt("10") * 2 }).toUnixInteger() / 1000;
const totalSeconds = (incrementDate - currentTime) / 1000;
setTopDate(incrementDate);
setCurrentValue(currentTime);
setTotalSeconds(totalSeconds);
let interval: any | null = null;
if (socket !== null) {
socket.onopen = () => {
console.log("WebSocket Client Connected");
socket.send("ok");
interval = setInterval(() => {
socket.send("ok");
}, 10 * 1000);
};
socket.onmessage = (message: MessageEvent) => {
const data: SpeedTestResponse = JSON.parse(message.data.toString());
setCurrStatus((prevStatus) => {
let prSt: SpeedTestResponse[] = [];
if (prevStatus) {
prSt = [...prevStatus];
}
const insertData = data.servers !== 0 ? [data] : [];
return [...prSt, ...insertData];
});
const currTime = DateTime.now().toUnixInteger() / 1000;
setCurrentValue(currTime);
};
socket.onclose = () => {
clearInterval(interval);
console.log("connection closed by server");
// reset start status
setStart(false);
};
return () => {
// close websocket on useEffect cleanup
socket.close(1000);
clearInterval(interval);
console.log("closing websockets");
};
}
} else {
// reset start status
setStart(false);
}
}, [size, sizeUnit, start, duration]);
useEffect(() => {
const actualSeconds = (topDate - currentValue) / 1000;
let percToDisplay = 100 - (actualSeconds * 100) / totalSeconds;
if (percToDisplay > 100) {
percToDisplay = 100;
}
setSpeedometerValue(percToDisplay);
}, [start, currentValue, topDate, totalSeconds]);
const stoppedLabel = currStatus !== null ? "Retest" : "Start";
const buttonLabel = start ? "Start" : stoppedLabel;
const startSpeedtestButton = () => {
if (!clusterRegistered) {
navigate("/support/register");
return;
}
setCurrStatus(null);
setStart(true);
};
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(setHelpName("performance"));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Fragment>
<PageHeaderWrapper label="Performance" actions={<HelpMenu />} />
<PageLayout>
{!clusterRegistered && <RegisterCluster compactMode />}
{!distributedSetup ? (
<DistributedOnly
iconComponent={<SpeedtestIcon />}
entity={"Speedtest"}
/>
) : (
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_HEAL]}
resource={CONSOLE_UI_RESOURCE}
>
<Box withBorders>
<Grid container>
<Grid item md={3} sm={12}>
<Box
sx={{
fontSize: 13,
marginBottom: 8,
}}
>
{start ? (
<Fragment>
Speedtest in progress...
<Loader style={{ width: 15, height: 15 }} />
</Fragment>
) : (
<Fragment>
{currStatus && !start ? (
<b>Speed Test results:</b>
) : (
<b>Performance test</b>
)}
</Fragment>
)}
</Box>
<Box>
<ProgressBarWrapper
value={speedometerValue}
ready={currStatus !== null && !start}
indeterminate={start}
size={"small"}
/>
</Box>
</Grid>
<Grid item md={4} sm={12}>
<div style={{ marginLeft: 10, width: 300 }}>
<InputBox
id={"size"}
name={"size"}
label={"Object Size"}
onChange={(e) => {
setSize(e.target.value);
}}
noLabelMinWidth={true}
value={size}
disabled={start || !clusterRegistered}
overlayObject={
<InputUnitMenu
id={"size-unit"}
onUnitChange={setSizeUnit}
unitSelected={sizeUnit}
unitsList={[
{ label: "KiB", value: "KiB" },
{ label: "MiB", value: "MiB" },
{ label: "GiB", value: "GiB" },
]}
disabled={start || !clusterRegistered}
/>
}
/>
</div>
</Grid>
<Grid item md={4} sm={12}>
<div style={{ marginLeft: 10, width: 300 }}>
<InputBox
id={"duration"}
name={"duration"}
label={"Duration"}
onChange={(e) => {
if (e.target.validity.valid) {
setDuration(e.target.value);
}
}}
noLabelMinWidth={true}
value={duration}
disabled={start || !clusterRegistered}
overlayObject={
<InputUnitMenu
id={"size-unit"}
onUnitChange={() => {}}
unitSelected={"s"}
unitsList={[{ label: "s", value: "s" }]}
disabled={start || !clusterRegistered}
/>
}
pattern={"[0-9]*"}
/>
</div>
</Grid>
<Grid item md={1} sm={12} sx={{ textAlign: "center" }}>
<Button
onClick={startSpeedtestButton}
color="primary"
type="button"
id={"start-speed-test"}
variant={
clusterRegistered && currStatus !== null && !start
? "callAction"
: "regular"
}
disabled={
duration.trim() === "" ||
size.trim() === "" ||
start ||
!clusterRegistered
}
label={buttonLabel}
/>
</Grid>
</Grid>
<Grid container>
<Grid item xs={12}>
<Fragment>
<Grid item xs={12}>
{currStatus !== null && (
<Fragment>
<STResults results={currStatus} start={start} />
</Fragment>
)}
</Grid>
</Fragment>
</Grid>
</Grid>
</Box>
{!start && !currStatus && clusterRegistered && (
<Fragment>
<br />
<HelpBox
title={
"During the speed test all your production traffic will be temporarily suspended."
}
iconComponent={<WarnIcon />}
help={<Fragment />}
/>
</Fragment>
)}
</SecureComponent>
)}
</PageLayout>
</Fragment>
);
};
export default Speedtest;

View File

@@ -1,48 +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 SpeedTestResponse {
version: string;
servers: number;
disks: number;
size: number;
concurrent: number;
PUTStats?: STStats;
GETStats?: STStats;
}
interface STStats {
throughputPerSec: number;
objectsPerSec: number;
servers: STServer[] | null;
}
export interface STServer {
endpoint: string;
throughputPerSec: number;
objectsPerSec: number;
err: string;
}
export interface IndvServerMetric {
host: string;
getValue: string;
getUnit: string;
getError?: string;
putValue: string;
putUnit: string;
putError?: string;
}

View File

@@ -1,32 +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 { SpeedTestResponse } from "./types";
export const cleanMetrics = (results: SpeedTestResponse[]) => {
const cleanRes = results.filter(
(item) => item.version !== "0" && item.disks !== 0,
);
const states = cleanRes.map((itemRes) => {
return {
get: itemRes.GETStats?.throughputPerSec || 0,
put: itemRes.PUTStats?.throughputPerSec || 0,
};
});
return [{ get: 0, put: 0 }, ...states];
};

View File

@@ -1,142 +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, { useCallback, useEffect, useState } from "react";
import { Box, Button, FormLayout, InputBox, OnlineRegistrationIcon } from "mds";
import { useNavigate } from "react-router-dom";
import { SubnetLoginRequest, SubnetLoginResponse } from "../License/types";
import { useAppDispatch } from "../../../store";
import {
setErrorSnackMessage,
setServerNeedsRestart,
} from "../../../systemSlice";
import { ErrorResponseHandler } from "../../../common/types";
import { modalStyleUtils } from "../Common/FormComponents/common/styleLibrary";
import { IAM_PAGES } from "../../../common/SecureComponent/permissions";
import GetApiKeyModal from "./GetApiKeyModal";
import RegisterHelpBox from "./RegisterHelpBox";
import api from "../../../common/api";
interface IApiKeyRegister {
registerEndpoint: string;
}
const ApiKeyRegister = ({ registerEndpoint }: IApiKeyRegister) => {
const navigate = useNavigate();
const [showApiKeyModal, setShowApiKeyModal] = useState(false);
const [apiKey, setApiKey] = useState("");
const [loading, setLoading] = useState(false);
const [fromModal, setFromModal] = useState(false);
const dispatch = useAppDispatch();
const onRegister = useCallback(() => {
if (loading) {
return;
}
setLoading(true);
let request: SubnetLoginRequest = { apiKey };
api
.invoke("POST", registerEndpoint, request)
.then((resp: SubnetLoginResponse) => {
setLoading(false);
if (resp && resp.registered) {
dispatch(setServerNeedsRestart(true));
navigate(IAM_PAGES.LICENSE);
}
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
setLoading(false);
reset();
});
}, [apiKey, dispatch, loading, registerEndpoint, navigate]);
useEffect(() => {
if (fromModal) {
onRegister();
}
}, [fromModal, onRegister]);
const reset = () => {
setApiKey("");
setFromModal(false);
};
return (
<FormLayout
title={"Register cluster with API key"}
icon={<OnlineRegistrationIcon />}
containerPadding={false}
withBorders={false}
helpBox={<RegisterHelpBox />}
>
<Box
sx={{
fontSize: 14,
display: "flex",
flexFlow: "column",
marginBottom: "30px",
}}
>
Use your MinIO Subscription Network API Key to register this cluster.
</Box>
<Box
sx={{
flex: "1",
}}
>
<InputBox
id="api-key"
name="api-key"
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setApiKey(event.target.value)
}
label="API Key"
value={apiKey}
/>
<Box sx={modalStyleUtils.modalButtonBar}>
<Button
id={"get-from-subnet"}
variant="regular"
disabled={loading}
onClick={() => setShowApiKeyModal(true)}
label={"Get from SUBNET"}
/>
<Button
id={"register"}
type="submit"
variant="callAction"
disabled={loading || apiKey.trim().length === 0}
onClick={() => onRegister()}
label={"Register"}
/>
</Box>
</Box>
<GetApiKeyModal
open={showApiKeyModal}
closeModal={() => setShowApiKeyModal(false)}
onSet={(value) => {
setApiKey(value);
setFromModal(true);
}}
/>
</FormLayout>
);
};
export default ApiKeyRegister;

View File

@@ -1,212 +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 {
Box,
Button,
CallHomeMenuIcon,
FormLayout,
HelpBox,
Loader,
PageLayout,
Switch,
} from "mds";
import { Link, useNavigate } from "react-router-dom";
import api from "../../../common/api";
import { ErrorResponseHandler } from "../../../common/types";
import { setErrorSnackMessage, setHelpName } from "../../../systemSlice";
import { useAppDispatch } from "../../../store";
import { ICallHomeResponse } from "./types";
import { registeredCluster } from "../../../config";
import CallHomeConfirmation from "./CallHomeConfirmation";
import RegisterCluster from "./RegisterCluster";
import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper";
import HelpMenu from "../HelpMenu";
const CallHome = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const [loading, setLoading] = useState<boolean>(true);
const [showConfirmation, setShowConfirmation] = useState<boolean>(false);
const [diagEnabled, setDiagEnabled] = useState<boolean>(false);
const [oDiagEnabled, setODiagEnabled] = useState<boolean>(false);
const [disableMode, setDisableMode] = useState<boolean>(false);
const clusterRegistered = registeredCluster();
useEffect(() => {
if (loading) {
api
.invoke("GET", `/api/v1/support/callhome`)
.then((res: ICallHomeResponse) => {
setLoading(false);
setDiagEnabled(!!res.diagnosticsStatus);
setODiagEnabled(!!res.diagnosticsStatus);
})
.catch((err: ErrorResponseHandler) => {
setLoading(false);
dispatch(setErrorSnackMessage(err));
});
}
}, [loading, dispatch]);
const callHomeClose = (refresh: boolean) => {
if (refresh) {
setLoading(true);
}
setShowConfirmation(false);
};
const confirmCallHomeAction = () => {
if (!clusterRegistered) {
navigate("/support/register");
return;
}
setDisableMode(false);
setShowConfirmation(true);
};
const disableCallHomeAction = () => {
setDisableMode(true);
setShowConfirmation(true);
};
let mainVariant: "regular" | "callAction" = "regular";
if (clusterRegistered && diagEnabled !== oDiagEnabled) {
mainVariant = "callAction";
}
useEffect(() => {
dispatch(setHelpName("call_home"));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Fragment>
{showConfirmation && (
<CallHomeConfirmation
onClose={callHomeClose}
open={showConfirmation}
diagStatus={diagEnabled}
disable={disableMode}
/>
)}
<PageHeaderWrapper label="Call Home" actions={<HelpMenu />} />
<PageLayout>
{!clusterRegistered && <RegisterCluster compactMode />}
<FormLayout
helpBox={
<HelpBox
title={"Learn more about Call Home"}
iconComponent={<CallHomeMenuIcon />}
help={
<Fragment>
<Box
sx={{
display: "flex",
flexFlow: "column",
fontSize: "14px",
flex: "2",
marginTop: "10px",
}}
>
<Box>
Enabling Call Home sends cluster health & status to your
registered MinIO Subscription Network account every 24
hours.
<br />
<br />
This helps the MinIO support team to provide quick
incident responses along with suggestions for possible
improvements that can be made to your MinIO instances.
<br />
<br />
Your cluster must be{" "}
<Link to={"/support/register"}>registered</Link> in the
MinIO Subscription Network (SUBNET) before enabling this
feature.
</Box>
</Box>
</Fragment>
}
/>
}
>
{loading ? (
<span style={{ marginLeft: 5 }}>
<Loader style={{ width: 16, height: 16 }} />
</span>
) : (
<Fragment>
<Switch
value="enableDiag"
id="enableDiag"
name="enableDiag"
checked={diagEnabled}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setDiagEnabled(event.target.checked);
}}
label={"Daily Health Report"}
disabled={!clusterRegistered}
description={
"Daily Health Report enables you to proactively identify potential issues in your deployment before they escalate."
}
/>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
marginTop: "55px",
gap: "0px 10px",
}}
>
{oDiagEnabled && (
<Button
id={"callhome-action"}
variant={"secondary"}
data-test-id="call-home-toggle-button"
onClick={disableCallHomeAction}
disabled={loading || !clusterRegistered}
>
Disable Call Home
</Button>
)}
<Button
id={"callhome-action"}
type="button"
variant={mainVariant}
data-test-id="call-home-toggle-button"
onClick={confirmCallHomeAction}
disabled={loading || !clusterRegistered}
>
Save Configuration
</Button>
</Box>
</Fragment>
)}
</FormLayout>
</PageLayout>
</Fragment>
);
};
export default CallHome;

View File

@@ -1,193 +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, useState } from "react";
import { Button, CallHomeMenuIcon, CircleIcon, Grid, ProgressBar } from "mds";
import api from "../../../common/api";
import { ICallHomeResponse } from "./types";
import { ErrorResponseHandler } from "../../../common/types";
import { setErrorSnackMessage, setSnackBarMessage } from "../../../systemSlice";
import { useAppDispatch } from "../../../store";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
interface ICallHomeConfirmation {
onClose: (refresh: boolean) => any;
open: boolean;
diagStatus: boolean;
disable?: boolean;
}
const CallHomeConfirmation = ({
onClose,
diagStatus,
open,
disable = false,
}: ICallHomeConfirmation) => {
const dispatch = useAppDispatch();
const [loading, setLoading] = useState<boolean>(false);
const onConfirmAction = () => {
setLoading(true);
api
.invoke("PUT", `/api/v1/support/callhome`, {
diagState: disable ? false : diagStatus,
logsState: false,
})
.then((res: ICallHomeResponse) => {
dispatch(setSnackBarMessage("Configuration saved successfully"));
setLoading(false);
onClose(true);
})
.catch((err: ErrorResponseHandler) => {
setLoading(false);
dispatch(setErrorSnackMessage(err));
});
};
return (
<ModalWrapper
modalOpen={open}
title={disable ? "Disable Call Home" : "Edit Call Home Configurations"}
onClose={() => onClose(false)}
titleIcon={<CallHomeMenuIcon />}
>
{disable ? (
<Fragment>
Please Acknowledge that after doing this action, we will no longer
receive updated cluster information automatically, losing the
potential benefits that Call Home provides to your MinIO cluster.
<Grid item xs={12} sx={{ margin: "15px 0" }}>
Are you sure you want to disable SUBNET Call Home?
</Grid>
<br />
{loading && (
<Grid
item
xs={12}
sx={{
marginBottom: 10,
}}
>
<ProgressBar />
</Grid>
)}
<Grid
item
xs={12}
sx={{
display: "flex",
justifyContent: "flex-end",
}}
>
<Button
id={"reset"}
type="button"
variant="regular"
disabled={loading}
onClick={() => onClose(false)}
label={"Cancel"}
sx={{
marginRight: 10,
}}
/>
<Button
id={"save-lifecycle"}
type="submit"
variant={"secondary"}
color="primary"
disabled={loading}
label={"Yes, Disable Call Home"}
onClick={onConfirmAction}
/>
</Grid>
</Fragment>
) : (
<Fragment>
Are you sure you want to change the following configurations for
SUBNET Call Home:
<Grid
item
sx={{
margin: "20px 0",
display: "flex",
flexDirection: "column",
gap: 15,
}}
>
<Grid item sx={{ display: "flex", alignItems: "center", gap: 10 }}>
<CircleIcon
style={{ fill: diagStatus ? "#4CCB92" : "#C83B51", width: 20 }}
/>
<span>
<strong>{diagStatus ? "Enable" : "Disable"}</strong> - Send
Diagnostics Information to SUBNET
</span>
</Grid>
</Grid>
<Grid item xs={12} sx={{ margin: "15px 0" }}>
Please Acknowledge that the information provided will only be
available in your SUBNET Account and it will not be shared to other
persons or entities besides MinIO team and you.
</Grid>
{loading && (
<Grid
item
xs={12}
sx={{
marginBottom: 10,
}}
>
<ProgressBar />
</Grid>
)}
<Grid
item
xs={12}
sx={{
display: "flex",
justifyContent: "flex-end",
}}
>
<Button
id={"reset"}
type="button"
variant="regular"
disabled={loading}
onClick={() => onClose(false)}
label={"Cancel"}
sx={{
marginRight: 10,
}}
/>
<Button
id={"save-lifecycle"}
type="submit"
variant={"callAction"}
color="primary"
disabled={loading}
label={"Yes, Save this Configuration"}
onClick={onConfirmAction}
/>
</Grid>
</Fragment>
)}
</ModalWrapper>
);
};
export default CallHomeConfirmation;

View File

@@ -1,86 +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 { Box, Button, FormLayout, Select } from "mds";
import { setLoading, setSelectedSubnetOrganization } from "./registerSlice";
import { useSelector } from "react-redux";
import { AppState, useAppDispatch } from "../../../store";
import { callRegister } from "./registerThunks";
import { modalStyleUtils } from "../Common/FormComponents/common/styleLibrary";
import RegisterHelpBox from "./RegisterHelpBox";
const ClusterRegistrationForm = () => {
const dispatch = useAppDispatch();
const subnetAccessToken = useSelector(
(state: AppState) => state.register.subnetAccessToken,
);
const selectedSubnetOrganization = useSelector(
(state: AppState) => state.register.selectedSubnetOrganization,
);
const subnetOrganizations = useSelector(
(state: AppState) => state.register.subnetOrganizations,
);
const loading = useSelector((state: AppState) => state.register.loading);
return (
<FormLayout
title={"Register MinIO cluster"}
containerPadding
withBorders={false}
helpBox={<RegisterHelpBox />}
>
<Select
id="subnet-organization"
name="subnet-organization"
onChange={(value) =>
dispatch(setSelectedSubnetOrganization(value as string))
}
label="Select an organization"
value={selectedSubnetOrganization}
options={subnetOrganizations.map((organization) => ({
label: organization.company,
value: organization.accountId.toString(),
}))}
/>
<Box sx={modalStyleUtils.modalButtonBar}>
<Button
id={"register-cluster"}
onClick={() => () => {
if (loading) {
return;
}
dispatch(setLoading(true));
if (subnetAccessToken && selectedSubnetOrganization) {
dispatch(
callRegister({
token: subnetAccessToken,
account_id: selectedSubnetOrganization,
}),
);
}
}}
disabled={loading || subnetAccessToken.trim().length === 0}
variant="callAction"
label={"Register"}
/>
</Box>
</FormLayout>
);
};
export default ClusterRegistrationForm;

View File

@@ -1,215 +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 { Box, FormLayout, InfoIcon, InputBox, LockIcon, UsersIcon } from "mds";
import { useAppDispatch } from "../../../store";
import { setErrorSnackMessage } from "../../../systemSlice";
import ConfirmDialog from "../Common/ModalWrapper/ConfirmDialog";
import { api } from "api";
import {
ApiError,
ApiKey,
HttpResponse,
SubnetLoginResponse,
} from "api/consoleApi";
import { errorToHandler } from "api/errors";
interface IGetApiKeyModalProps {
open: boolean;
closeModal: () => void;
onSet: (apiKey: string) => void;
}
const GetApiKeyModal = ({ open, closeModal, onSet }: IGetApiKeyModalProps) => {
const dispatch = useAppDispatch();
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState("");
const [mfaToken, setMfaToken] = useState("");
const [subnetOTP, setSubnetOTP] = useState("");
const [loadingSave, setLoadingSave] = useState<boolean>(false);
const onError = (err: ApiError) => {
dispatch(setErrorSnackMessage(errorToHandler(err)));
closeModal();
setEmail("");
setPassword("");
setMfaToken("");
setSubnetOTP("");
};
const onConfirm = () => {
if (mfaToken !== "") {
submitSubnetMfa();
} else {
submitSubnetLogin();
}
};
const submitSubnetMfa = () => {
setLoadingSave(true);
api.subnet
.subnetLoginMfa({
username: email,
otp: subnetOTP,
mfa_token: mfaToken,
})
.then((res: HttpResponse<SubnetLoginResponse, ApiError>) => {
if (res.data && res.data.access_token) {
getApiKey(res.data.access_token);
}
})
.catch((res: HttpResponse<SubnetLoginResponse, ApiError>) => {
onError(res.error);
})
.finally(() => setLoadingSave(false));
};
const getApiKey = (access_token: string) => {
setLoadingSave(true);
api.subnet
.subnetApiKey({
token: access_token,
})
.then((res: HttpResponse<ApiKey, ApiError>) => {
if (res.data && res.data.apiKey) {
onSet(res.data.apiKey);
closeModal();
}
})
.catch((res: HttpResponse<SubnetLoginResponse, ApiError>) => {
onError(res.error);
})
.finally(() => setLoadingSave(false));
};
const submitSubnetLogin = () => {
setLoadingSave(true);
api.subnet
.subnetLogin({ username: email, password })
.then((res: HttpResponse<SubnetLoginResponse, ApiError>) => {
if (res.data && res.data.mfa_token) {
setMfaToken(res.data.mfa_token);
}
})
.catch((res: HttpResponse<SubnetLoginResponse, ApiError>) => {
onError(res.error);
})
.finally(() => setLoadingSave(false));
};
const getDialogContent = () => {
if (mfaToken === "") {
return getCredentialsDialog();
}
return getMFADialog();
};
const getCredentialsDialog = () => {
return (
<FormLayout withBorders={false} containerPadding={false}>
<InputBox
id="subnet-email"
name="subnet-email"
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setEmail(event.target.value)
}
label="Email"
value={email}
overlayIcon={<UsersIcon />}
/>
<InputBox
id="subnet-password"
name="subnet-password"
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setPassword(event.target.value)
}
label="Password"
type={"password"}
value={password}
/>
</FormLayout>
);
};
const getMFADialog = () => {
return (
<Box sx={{ display: "flex" }}>
<Box sx={{ display: "flex", flexFlow: "column", flex: "2" }}>
<Box
sx={{
fontSize: 14,
display: "flex",
flexFlow: "column",
marginTop: 20,
marginBottom: 20,
}}
>
Two-Factor Authentication
</Box>
<Box>
Please enter the 6-digit verification code that was sent to your
email address. This code will be valid for 5 minutes.
</Box>
<Box
sx={{
flex: "1",
marginTop: "30px",
}}
>
<InputBox
overlayIcon={<LockIcon />}
id="subnet-otp"
name="subnet-otp"
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setSubnetOTP(event.target.value)
}
placeholder=""
label=""
value={subnetOTP}
/>
</Box>
</Box>
</Box>
);
};
return open ? (
<ConfirmDialog
title={"Get API Key from SUBNET"}
confirmText={"Get API Key"}
isOpen={open}
titleIcon={<InfoIcon />}
isLoading={loadingSave}
cancelText={"Cancel"}
onConfirm={onConfirm}
onClose={closeModal}
confirmButtonProps={{
variant: "callAction",
disabled: !email || !password || loadingSave,
hidden: true,
}}
cancelButtonProps={{
disabled: loadingSave,
}}
confirmationContent={getDialogContent()}
/>
) : null;
};
export default GetApiKeyModal;

View File

@@ -1,193 +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, useState } from "react";
import {
Box,
Button,
CommentBox,
CopyIcon,
FormLayout,
OfflineRegistrationIcon,
} from "mds";
import { ClusterRegistered } from "./utils";
import { AppState, useAppDispatch } from "../../../store";
import { useSelector } from "react-redux";
import { fetchLicenseInfo } from "./registerThunks";
import {
setErrorSnackMessage,
setServerNeedsRestart,
} from "../../../systemSlice";
import { modalStyleUtils } from "../Common/FormComponents/common/styleLibrary";
import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper";
import CopyToClipboard from "react-copy-to-clipboard";
import RegisterHelpBox from "./RegisterHelpBox";
import { api } from "api";
import { ApiError, HttpResponse, SetConfigResponse } from "api/consoleApi";
import { errorToHandler } from "api/errors";
const OfflineRegistration = () => {
const dispatch = useAppDispatch();
const subnetRegToken = useSelector(
(state: AppState) => state.register.subnetRegToken,
);
const clusterRegistered = useSelector(
(state: AppState) => state.register.clusterRegistered,
);
const licenseInfo = useSelector(
(state: AppState) => state.register.licenseInfo,
);
const offlineRegUrl = `https://subnet.min.io/cluster/register?token=${subnetRegToken}`;
const [licenseKey, setLicenseKey] = useState("");
const [loadingSave, setLoadingSave] = useState<boolean>(false);
const applyAirGapLicense = () => {
setLoadingSave(true);
api.configs
.setConfig("subnet", {
key_values: [{ key: "license", value: licenseKey }],
})
.then((_) => {
dispatch(fetchLicenseInfo());
dispatch(setServerNeedsRestart(true));
})
.catch((res: HttpResponse<SetConfigResponse, ApiError>) => {
dispatch(setErrorSnackMessage(errorToHandler(res.error)));
})
.finally(() => setLoadingSave(false));
};
return (
<Fragment>
<Box
withBorders
sx={{
display: "flex",
flexFlow: "column",
padding: "43px",
}}
>
{clusterRegistered && licenseInfo ? (
<ClusterRegistered email={licenseInfo.email} />
) : (
<FormLayout
title={"Register cluster in an Air-gap environment"}
icon={<OfflineRegistrationIcon />}
helpBox={<RegisterHelpBox />}
withBorders={false}
containerPadding={false}
>
<Box
sx={{
display: "flex",
flexFlow: "column",
flex: "2",
marginTop: "15px",
"& .step-row": {
fontSize: 14,
display: "flex",
marginTop: "15px",
marginBottom: "15px",
},
}}
>
<Box>
<Box className="step-row">
<Box className="step-text">
Click on the link to register this cluster in SUBNET and get
a License Key for this Air-Gap deployment
</Box>
</Box>
<Box
sx={{
flex: "1",
display: "flex",
alignItems: "center",
gap: 3,
}}
>
<a href={offlineRegUrl} target="_blank">
https://subnet.min.io/cluster/register
</a>
<TooltipWrapper tooltip={"Copy to Clipboard"}>
<CopyToClipboard text={offlineRegUrl}>
<Button
type={"button"}
id={"copy-ult-to-clip-board"}
icon={<CopyIcon />}
color={"primary"}
variant={"regular"}
/>
</CopyToClipboard>
</TooltipWrapper>
</Box>
<Box
className={"muted"}
sx={{
marginTop: "25px",
}}
>
Note: If this machine does not have internet connection, Copy
paste the following URL in a browser where you access SUBNET
and follow the instructions to complete the registration
</Box>
<Box
sx={{
marginTop: "25px",
display: "flex",
flexDirection: "column",
}}
>
<label style={{ fontWeight: "bold", marginBottom: "10px" }}>
Paste the License Key{" "}
</label>
<CommentBox
value={licenseKey}
disabled={loadingSave}
label={""}
id={"licenseKey"}
name={"licenseKey"}
placeholder={"License Key"}
onChange={(e) => {
setLicenseKey(e.target.value);
}}
/>
</Box>
<Box sx={modalStyleUtils.modalButtonBar}>
<Button
id={"apply-license-key"}
onClick={applyAirGapLicense}
variant={"callAction"}
disabled={!licenseKey || loadingSave}
label={"Apply Cluster License"}
/>
</Box>
</Box>
</Box>
</FormLayout>
)}
</Box>
</Fragment>
);
};
export default OfflineRegistration;

View File

@@ -1,118 +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 {
Box,
Button,
FormLayout,
InputBox,
OnlineRegistrationIcon,
UsersIcon,
} from "mds";
import RegisterHelpBox from "./RegisterHelpBox";
import { modalStyleUtils } from "../Common/FormComponents/common/styleLibrary";
import { useSelector } from "react-redux";
import { AppState, useAppDispatch } from "../../../store";
import { setSubnetEmail, setSubnetPassword } from "./registerSlice";
import { subnetLogin } from "./registerThunks";
const OnlineRegistration = () => {
const dispatch = useAppDispatch();
const subnetPassword = useSelector(
(state: AppState) => state.register.subnetPassword,
);
const subnetEmail = useSelector(
(state: AppState) => state.register.subnetEmail,
);
const loading = useSelector((state: AppState) => state.register.loading);
return (
<FormLayout
icon={<OnlineRegistrationIcon />}
title={"Online activation of MinIO Subscription Network License"}
withBorders={false}
containerPadding={false}
helpBox={<RegisterHelpBox />}
>
<Box
sx={{
fontSize: "14px",
display: "flex",
flexFlow: "column",
marginBottom: "30px",
}}
>
Use your MinIO Subscription Network login credentials to register this
cluster.
</Box>
<Box
sx={{
flex: "1",
}}
>
<InputBox
id="subnet-email"
name="subnet-email"
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
dispatch(setSubnetEmail(event.target.value))
}
label="Email"
value={subnetEmail}
overlayIcon={<UsersIcon />}
/>
<InputBox
id="subnet-password"
name="subnet-password"
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
dispatch(setSubnetPassword(event.target.value))
}
label="Password"
type={"password"}
value={subnetPassword}
/>
<Box sx={modalStyleUtils.modalButtonBar}>
<Button
id={"sign-up"}
type="submit"
variant="regular"
onClick={(e) => {
e.preventDefault();
window.open(`https://min.io/signup?ref=con`, "_blank");
}}
label={"Sign up"}
/>
<Button
id={"register-credentials"}
type="submit"
variant="callAction"
disabled={
loading ||
subnetEmail.trim().length === 0 ||
subnetPassword.trim().length === 0
}
onClick={() => dispatch(subnetLogin())}
label={"Register"}
/>
</Box>
</Box>
</FormLayout>
);
};
export default OnlineRegistration;

View File

@@ -1,186 +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 { Button, PageLayout, FormLayout, Box, Checkbox, InputLabel } from "mds";
import { wsProtocol } from "../../../utils/wsUtils";
import { useNavigate } from "react-router-dom";
import { registeredCluster } from "../../../config";
import { useAppDispatch } from "../../../store";
import { setHelpName } from "../../../systemSlice";
import RegisterCluster from "./RegisterCluster";
import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper";
import HelpMenu from "../HelpMenu";
var socket: any = null;
const Profile = () => {
const navigate = useNavigate();
const [profilingStarted, setProfilingStarted] = useState<boolean>(false);
const [types, setTypes] = useState<string[]>([
"cpu",
"mem",
"block",
"mutex",
"goroutines",
]);
const clusterRegistered = registeredCluster();
const typesList = [
{ label: "cpu", value: "cpu" },
{ label: "mem", value: "mem" },
{ label: "block", value: "block" },
{ label: "mutex", value: "mutex" },
{ label: "goroutines", value: "goroutines" },
];
const onCheckboxClick = (e: React.ChangeEvent<HTMLInputElement>) => {
let newArr: string[] = [];
if (types.indexOf(e.target.value) > -1) {
newArr = types.filter((type) => type !== e.target.value);
} else {
newArr = [...types, e.target.value];
}
setTypes(newArr);
};
const startProfiling = () => {
const typeString = types.join(",");
const url = new URL(window.location.toString());
const isDev = process.env.NODE_ENV === "development";
const port = isDev ? "9090" : url.port;
// check if we are using base path, if not this always is `/`
const baseLocation = new URL(document.baseURI);
const baseUrl = baseLocation.pathname;
const wsProt = wsProtocol(url.protocol);
socket = new WebSocket(
`${wsProt}://${url.hostname}:${port}${baseUrl}ws/profile?types=${typeString}`,
);
if (socket !== null) {
socket.onopen = () => {
setProfilingStarted(true);
socket.send("ok");
};
socket.onmessage = (message: MessageEvent) => {
// process received message
let response = new Blob([message.data], { type: "application/zip" });
let filename = "profile.zip";
setProfilingStarted(false);
var link = document.createElement("a");
link.href = window.URL.createObjectURL(response);
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
socket.onclose = () => {
console.log("connection closed by server");
setProfilingStarted(false);
};
return () => {
socket.close(1000);
console.log("closing websockets");
setProfilingStarted(false);
};
}
};
const stopProfiling = () => {
socket.close(1000);
setProfilingStarted(false);
};
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(setHelpName("profile"));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Fragment>
<PageHeaderWrapper label="Profile" actions={<HelpMenu />} />
<PageLayout>
{!clusterRegistered && <RegisterCluster compactMode />}
<FormLayout>
<Box
sx={{
display: "flex",
gap: 10,
"& div": { width: "initial" },
"& .inputItem:not(:last-of-type)": { marginBottom: 0 },
}}
>
<InputLabel noMinWidth>Types to profile:</InputLabel>
{typesList.map((t) => (
<Checkbox
checked={types.indexOf(t.value) > -1}
disabled={profilingStarted || !clusterRegistered}
key={`checkbox-${t.label}`}
id={`checkbox-${t.label}`}
label={t.label}
name={`checkbox-${t.label}`}
onChange={onCheckboxClick}
value={t.value}
/>
))}
</Box>
<Box
sx={{
display: "flex",
justifyContent: "flex-end",
marginTop: 24,
gap: 10,
}}
>
<Button
id={"start-profiling"}
type="submit"
variant={clusterRegistered ? "callAction" : "regular"}
disabled={
profilingStarted || types.length < 1 || !clusterRegistered
}
onClick={() => {
if (!clusterRegistered) {
navigate("/support/register");
return;
}
startProfiling();
}}
label={"Start Profiling"}
/>
<Button
id={"stop-profiling"}
type="submit"
variant="callAction"
color="primary"
disabled={!profilingStarted || !clusterRegistered}
onClick={() => {
stopProfiling();
}}
label={"Stop Profiling"}
/>
</Box>
</FormLayout>
</PageLayout>
</Fragment>
);
};
export default Profile;

View File

@@ -1,213 +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 { Box, PageLayout, Tabs } from "mds";
import { SubnetRegTokenResponse } from "../License/types";
import { ErrorResponseHandler } from "../../../common/types";
import { useSelector } from "react-redux";
import { setErrorSnackMessage, setHelpName } from "../../../systemSlice";
import { AppState, useAppDispatch } from "../../../store";
import { ClusterRegistered, ProxyConfiguration } from "./utils";
import { fetchLicenseInfo } from "./registerThunks";
import {
resetRegisterForm,
setCurTab,
setLoading,
setSubnetRegToken,
} from "./registerSlice";
import OfflineRegistration from "./OfflineRegistration";
import SubnetMFAToken from "./SubnetMFAToken";
import ClusterRegistrationForm from "./ClusterRegistrationForm";
import OnlineRegistration from "./OnlineRegistration";
import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper";
import HelpMenu from "../HelpMenu";
import api from "../../../common/api";
import ApiKeyRegister from "./ApiKeyRegister";
const Register = () => {
const dispatch = useAppDispatch();
const subnetMFAToken = useSelector(
(state: AppState) => state.register.subnetMFAToken,
);
const subnetAccessToken = useSelector(
(state: AppState) => state.register.subnetAccessToken,
);
const subnetRegToken = useSelector(
(state: AppState) => state.register.subnetRegToken,
);
const subnetOrganizations = useSelector(
(state: AppState) => state.register.subnetOrganizations,
);
const loading = useSelector((state: AppState) => state.register.loading);
const loadingLicenseInfo = useSelector(
(state: AppState) => state.register.loadingLicenseInfo,
);
const clusterRegistered = useSelector(
(state: AppState) => state.register.clusterRegistered,
);
const licenseInfo = useSelector(
(state: AppState) => state.register.licenseInfo,
);
const curTab = useSelector((state: AppState) => state.register.curTab);
const [initialLicenseLoading, setInitialLicenseLoading] =
useState<boolean>(true);
useEffect(() => {
// when unmounted, reset
return () => {
dispatch(resetRegisterForm());
};
}, [dispatch]);
useEffect(() => {
if (curTab === "simple-tab-2" && !loading && !subnetRegToken) {
const fetchSubnetRegToken = () => {
dispatch(setLoading(true));
api
.invoke("GET", "/api/v1/subnet/registration-token")
.then((resp: SubnetRegTokenResponse) => {
dispatch(setLoading(false));
if (resp && resp.regToken) {
dispatch(setSubnetRegToken(resp.regToken));
}
})
.catch((err: ErrorResponseHandler) => {
console.error(err);
dispatch(setErrorSnackMessage(err));
dispatch(setLoading(false));
});
};
fetchSubnetRegToken();
}
}, [curTab, loading, subnetRegToken, dispatch]);
useEffect(() => {
if (initialLicenseLoading) {
dispatch(fetchLicenseInfo());
setInitialLicenseLoading(false);
}
}, [initialLicenseLoading, setInitialLicenseLoading, dispatch]);
let clusterRegistrationForm: React.ReactElement = <Fragment />;
if (subnetAccessToken && subnetOrganizations.length > 0) {
clusterRegistrationForm = <ClusterRegistrationForm />;
} else if (subnetMFAToken) {
clusterRegistrationForm = <SubnetMFAToken />;
} else {
clusterRegistrationForm = <OnlineRegistration />;
}
const apiKeyRegistration = (
<Fragment>
<Box
withBorders
sx={{
display: "flex",
flexFlow: "column",
padding: "43px",
}}
>
{clusterRegistered && licenseInfo ? (
<ClusterRegistered email={licenseInfo.email} />
) : (
<ApiKeyRegister registerEndpoint={"/api/v1/subnet/login"} />
)}
</Box>
<ProxyConfiguration />
</Fragment>
);
const offlineRegistration = <OfflineRegistration />;
const regUi = (
<Fragment>
<Box
withBorders
sx={{
display: "flex",
flexFlow: "column",
padding: "43px",
}}
>
{clusterRegistered && licenseInfo ? (
<ClusterRegistered email={licenseInfo.email} />
) : (
clusterRegistrationForm
)}
</Box>
{!clusterRegistered && <ProxyConfiguration />}
</Fragment>
);
const loadingUi = <div>Loading..</div>;
const uiToShow = loadingLicenseInfo ? loadingUi : regUi;
useEffect(() => {
dispatch(setHelpName("register"));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Fragment>
<PageHeaderWrapper
label="Register to MinIO Subscription Network"
actions={<HelpMenu />}
/>
<PageLayout>
<Tabs
horizontal
currentTabOrPath={curTab}
onTabClick={(newValue: string) => {
dispatch(setCurTab(newValue));
}}
options={[
{
tabConfig: {
label: "Credentials",
id: "simple-tab-0",
},
content: uiToShow,
},
{
tabConfig: {
label: "API Key",
id: "simple-tab-1",
},
content: apiKeyRegistration,
},
{
tabConfig: {
label: "Air-Gap",
id: "simple-tab-2",
},
content: offlineRegistration,
},
]}
/>
</PageLayout>
</Fragment>
);
};
export default Register;

View File

@@ -1,162 +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 { useNavigate } from "react-router-dom";
import { Box, breakPoints, Button, Grid, HelpBox, WarnIcon } from "mds";
interface IRegisterCluster {
compactMode?: boolean;
}
const RegisterCluster = ({ compactMode = false }: IRegisterCluster) => {
const navigate = useNavigate();
const redirectButton = (
<Button
id={"go-to-register"}
type="submit"
variant="callAction"
color="primary"
onClick={() => navigate("/support/register")}
>
Register your Cluster
</Button>
);
const registerMessage =
"Please use your MinIO Subscription Network login credentials to register this cluster and enable this feature.";
if (compactMode) {
return (
<Fragment>
<Grid
sx={{
"& div.leftItems": {
marginBottom: 0,
},
}}
>
<HelpBox
title={
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
flexGrow: 1,
}}
>
<span>{registerMessage}</span> {redirectButton}
</div>
}
iconComponent={<WarnIcon />}
help={null}
/>
</Grid>
<br />
</Fragment>
);
}
return (
<Box
sx={{
padding: "25px",
border: "1px solid #eaeaea",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexFlow: "row",
marginBottom: "15px",
[`@media (max-width: ${breakPoints.sm}px)`]: {
flexFlow: "column",
},
}}
>
<Grid container>
<Grid item xs={12}>
<Box
sx={{
marginRight: "8px",
fontSize: "16px",
fontWeight: 600,
display: "flex",
alignItems: "center",
"& .min-icon": {
width: "83px",
height: "14px",
marginLeft: "5px",
marginRight: "5px",
},
}}
>
Register your cluster
</Box>
</Grid>
<Grid item xs={12}>
<Box
sx={{
display: "flex",
flexFlow: "row",
[`@media (max-width: ${breakPoints.sm}px)`]: {
flexFlow: "column",
},
}}
>
<Box
sx={{
display: "flex",
flexFlow: "column",
flex: "2",
}}
>
<Box
sx={{
fontSize: "16px",
display: "flex",
flexFlow: "column",
marginTop: "15px",
marginBottom: "15px",
}}
>
{registerMessage}
</Box>
<Box
sx={{
flex: "1",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
}}
>
{redirectButton}
</Box>
</Box>
</Box>
</Box>
</Grid>
</Grid>
</Box>
);
};
export default RegisterCluster;

View File

@@ -1,100 +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 {
CallHomeFeatureIcon,
DiagnosticsFeatureIcon,
ExtraFeaturesIcon,
HelpIconFilled,
PerformanceFeatureIcon,
Box,
HelpBox,
} from "mds";
const FeatureItem = ({
icon,
description,
}: {
icon: any;
description: string | React.ReactNode;
}) => {
return (
<Box
sx={{
display: "flex",
"& .min-icon": {
marginRight: "10px",
height: "23px",
width: "23px",
marginBottom: "10px",
},
}}
>
{icon}{" "}
<Box className="muted" style={{ fontSize: "14px", fontStyle: "italic" }}>
{description}
</Box>
</Box>
);
};
const RegisterHelpBox = () => {
return (
<HelpBox
title={"Why should I register?"}
iconComponent={<HelpIconFilled />}
help={
<Fragment>
<Box sx={{ fontSize: "14px", marginBottom: "15px" }}>
Registering this cluster with the MinIO Subscription Network
(SUBNET) provides the following benefits in addition to the
commercial license and SLA backed support.
</Box>
<Box
sx={{
display: "flex",
flexFlow: "column",
}}
>
<FeatureItem
icon={<CallHomeFeatureIcon />}
description={`Call Home Monitoring`}
/>
<FeatureItem
icon={<DiagnosticsFeatureIcon />}
description={`Health Diagnostics`}
/>
<FeatureItem
icon={<PerformanceFeatureIcon />}
description={`Performance Analysis`}
/>
<FeatureItem
icon={<ExtraFeaturesIcon />}
description={
<a href="https://min.io/signup?ref=con" target="_blank">
More Features
</a>
}
/>
</Box>
</Fragment>
}
/>
);
};
export default RegisterHelpBox;

View File

@@ -1,81 +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 { VerifiedIcon, Box, breakPoints } from "mds";
const RegistrationStatusBanner = ({ email = "" }: { email?: string }) => {
return (
<Box
sx={{
height: 67,
color: "#ffffff",
display: "flex",
position: "relative",
top: -30,
left: -32,
width: "calc(100% + 64px)",
alignItems: "center",
justifyContent: "space-between",
backgroundColor: "#2781B0",
padding: "0 25px 0 25px",
"& .registered-box, .reg-badge-box": {
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
},
"& .reg-badge-box": {
marginLeft: "20px",
"& .min-icon": {
fill: "#2781B0",
},
},
}}
>
<Box className="registered-box">
<Box sx={{ fontSize: "16px", fontWeight: 400 }}>Register status:</Box>
<Box className="reg-badge-box">
<VerifiedIcon />
<Box
sx={{
fontWeight: 600,
}}
>
Registered
</Box>
</Box>
</Box>
<Box
className="registered-acc-box"
sx={{
alignItems: "center",
justifyContent: "flex-start",
display: "flex",
[`@media (max-width: ${breakPoints.sm}px)`]: {
display: "none",
},
}}
>
<Box sx={{ fontSize: "16px", fontWeight: 400 }}>Registered to:</Box>
<Box sx={{ marginLeft: "8px", fontWeight: 600 }}>{email}</Box>
</Box>
</Box>
);
};
export default RegistrationStatusBanner;

View File

@@ -1,83 +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 { Box, Button, FormLayout, InputBox, LockIcon } from "mds";
import { useSelector } from "react-redux";
import { setSubnetOTP } from "./registerSlice";
import { AppState, useAppDispatch } from "../../../store";
import { subnetLoginWithMFA } from "./registerThunks";
import { modalStyleUtils } from "../Common/FormComponents/common/styleLibrary";
import RegisterHelpBox from "./RegisterHelpBox";
const SubnetMFAToken = () => {
const dispatch = useAppDispatch();
const subnetMFAToken = useSelector(
(state: AppState) => state.register.subnetMFAToken,
);
const subnetOTP = useSelector((state: AppState) => state.register.subnetOTP);
const loading = useSelector((state: AppState) => state.register.loading);
return (
<FormLayout
title={"Two-Factor Authentication"}
helpBox={<RegisterHelpBox />}
withBorders={false}
containerPadding={false}
>
<Box
sx={{
fontSize: 14,
display: "flex",
flexFlow: "column",
marginBottom: "30px",
}}
>
Please enter the 6-digit verification code that was sent to your email
address. This code will be valid for 5 minutes.
</Box>
<Box>
<InputBox
overlayIcon={<LockIcon />}
id="subnet-otp"
name="subnet-otp"
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
dispatch(setSubnetOTP(event.target.value))
}
placeholder=""
label=""
value={subnetOTP}
/>
</Box>
<Box sx={modalStyleUtils.modalButtonBar}>
<Button
id={"verify"}
onClick={() => dispatch(subnetLoginWithMFA())}
disabled={
loading ||
subnetOTP.trim().length === 0 ||
subnetMFAToken.trim().length === 0
}
variant="callAction"
label={"Verify"}
/>
</Box>
</FormLayout>
);
};
export default SubnetMFAToken;

View File

@@ -1,125 +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 { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { SubnetInfo, SubnetOrganization } from "../License/types";
interface RegisterState {
license: string;
subnetPassword: string;
subnetEmail: string;
subnetMFAToken: string;
subnetOTP: string;
subnetAccessToken: string;
selectedSubnetOrganization: string;
subnetRegToken: string;
subnetOrganizations: SubnetOrganization[];
loading: boolean;
loadingLicenseInfo: boolean;
clusterRegistered: boolean;
licenseInfo: SubnetInfo | undefined;
curTab: string;
}
const initialState: RegisterState = {
license: "",
subnetPassword: "",
subnetEmail: "",
subnetMFAToken: "",
subnetOTP: "",
subnetAccessToken: "",
selectedSubnetOrganization: "",
subnetRegToken: "",
subnetOrganizations: [],
loading: false,
loadingLicenseInfo: false,
clusterRegistered: false,
licenseInfo: undefined,
curTab: "simple-tab-0",
};
const registerSlice = createSlice({
name: "register",
initialState,
reducers: {
setLicense: (state, action: PayloadAction<string>) => {
state.license = action.payload;
},
setSubnetPassword: (state, action: PayloadAction<string>) => {
state.subnetPassword = action.payload;
},
setSubnetEmail: (state, action: PayloadAction<string>) => {
state.subnetEmail = action.payload;
},
setSubnetMFAToken: (state, action: PayloadAction<string>) => {
state.subnetMFAToken = action.payload;
},
setSubnetOTP: (state, action: PayloadAction<string>) => {
state.subnetOTP = action.payload;
},
setSubnetAccessToken: (state, action: PayloadAction<string>) => {
state.subnetAccessToken = action.payload;
},
setSelectedSubnetOrganization: (state, action: PayloadAction<string>) => {
state.selectedSubnetOrganization = action.payload;
},
setSubnetRegToken: (state, action: PayloadAction<string>) => {
state.subnetRegToken = action.payload;
},
setSubnetOrganizations: (
state,
action: PayloadAction<SubnetOrganization[]>,
) => {
state.subnetOrganizations = action.payload;
},
setLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload;
},
setLoadingLicenseInfo: (state, action: PayloadAction<boolean>) => {
state.loadingLicenseInfo = action.payload;
},
setClusterRegistered: (state, action: PayloadAction<boolean>) => {
state.clusterRegistered = action.payload;
},
setLicenseInfo: (state, action: PayloadAction<SubnetInfo | undefined>) => {
state.licenseInfo = action.payload;
},
setCurTab: (state, action: PayloadAction<string>) => {
state.curTab = action.payload;
},
resetRegisterForm: () => initialState,
},
});
// Action creators are generated for each case reducer function
export const {
setSubnetPassword,
setSubnetEmail,
setSubnetMFAToken,
setSubnetOTP,
setSubnetAccessToken,
setSelectedSubnetOrganization,
setSubnetRegToken,
setSubnetOrganizations,
setLoading,
setLoadingLicenseInfo,
setClusterRegistered,
setLicenseInfo,
setCurTab,
resetRegisterForm,
} = registerSlice.actions;
export default registerSlice.reducer;

View File

@@ -1,215 +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 {
resetRegisterForm,
setClusterRegistered,
setLicenseInfo,
setLoading,
setLoadingLicenseInfo,
setSelectedSubnetOrganization,
setSubnetAccessToken,
setSubnetMFAToken,
setSubnetOrganizations,
setSubnetOTP,
} from "./registerSlice";
import api from "../../../common/api";
import {
SubnetInfo,
SubnetLoginRequest,
SubnetLoginResponse,
SubnetLoginWithMFARequest,
SubnetRegisterRequest,
} from "../License/types";
import { ErrorResponseHandler } from "../../../common/types";
import {
setErrorSnackMessage,
setServerNeedsRestart,
} from "../../../systemSlice";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { AppState } from "../../../store";
import { hasPermission } from "../../../common/SecureComponent";
import {
CONSOLE_UI_RESOURCE,
IAM_PAGES,
IAM_PAGES_PERMISSIONS,
} from "../../../common/SecureComponent/permissions";
export const fetchLicenseInfo = createAsyncThunk(
"register/fetchLicenseInfo",
async (_, { getState, dispatch }) => {
const state = getState() as AppState;
const getSubnetInfo = hasPermission(
CONSOLE_UI_RESOURCE,
IAM_PAGES_PERMISSIONS[IAM_PAGES.LICENSE],
true,
);
const loadingLicenseInfo = state.register.loadingLicenseInfo;
if (loadingLicenseInfo) {
return;
}
if (getSubnetInfo) {
dispatch(setLoadingLicenseInfo(true));
api
.invoke("GET", `/api/v1/subnet/info`)
.then((res: SubnetInfo) => {
dispatch(setLicenseInfo(res));
dispatch(setClusterRegistered(true));
dispatch(setLoadingLicenseInfo(false));
})
.catch((err: ErrorResponseHandler) => {
if (
err.detailedError.toLowerCase() !==
"License is not present".toLowerCase() &&
err.detailedError.toLowerCase() !==
"license not found".toLowerCase()
) {
dispatch(setErrorSnackMessage(err));
}
dispatch(setClusterRegistered(false));
dispatch(setLoadingLicenseInfo(false));
});
} else {
dispatch(setLoadingLicenseInfo(false));
}
},
);
interface ClassRegisterArgs {
token: string;
account_id: string;
}
export const callRegister = createAsyncThunk(
"register/callRegister",
async (args: ClassRegisterArgs, { dispatch }) => {
const request: SubnetRegisterRequest = {
token: args.token,
account_id: args.account_id,
};
api
.invoke("POST", "/api/v1/subnet/register", request)
.then(() => {
dispatch(setLoading(false));
dispatch(setServerNeedsRestart(true));
dispatch(resetRegisterForm());
dispatch(fetchLicenseInfo());
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
dispatch(setLoading(false));
});
},
);
export const subnetLoginWithMFA = createAsyncThunk(
"register/subnetLoginWithMFA",
async (_, { getState, rejectWithValue, dispatch }) => {
const state = getState() as AppState;
const subnetEmail = state.register.subnetEmail;
const subnetMFAToken = state.register.subnetMFAToken;
const subnetOTP = state.register.subnetOTP;
const loading = state.register.loading;
if (loading) {
return;
}
dispatch(setLoading(true));
const request: SubnetLoginWithMFARequest = {
username: subnetEmail,
otp: subnetOTP,
mfa_token: subnetMFAToken,
};
api
.invoke("POST", "/api/v1/subnet/login/mfa", request)
.then((resp: SubnetLoginResponse) => {
dispatch(setLoading(false));
if (resp && resp.access_token && resp.organizations.length > 0) {
if (resp.organizations.length === 1) {
dispatch(
callRegister({
token: resp.access_token,
account_id: resp.organizations[0].accountId.toString(),
}),
);
} else {
dispatch(setSubnetAccessToken(resp.access_token));
dispatch(setSubnetOrganizations(resp.organizations));
dispatch(
setSelectedSubnetOrganization(
resp.organizations[0].accountId.toString(),
),
);
}
}
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
dispatch(setLoading(false));
dispatch(setSubnetOTP(""));
});
},
);
export const subnetLogin = createAsyncThunk(
"register/subnetLogin",
async (_, { getState, rejectWithValue, dispatch }) => {
const state = getState() as AppState;
const license = state.register.license;
const subnetPassword = state.register.subnetPassword;
const subnetEmail = state.register.subnetEmail;
const loading = state.register.loading;
if (loading) {
return;
}
dispatch(setLoading(true));
let request: SubnetLoginRequest = {
username: subnetEmail,
password: subnetPassword,
apiKey: license,
};
api
.invoke("POST", "/api/v1/subnet/login", request)
.then((resp: SubnetLoginResponse) => {
dispatch(setLoading(false));
if (resp && resp.registered) {
dispatch(resetRegisterForm());
dispatch(fetchLicenseInfo());
} else if (resp && resp.mfa_token) {
dispatch(setSubnetMFAToken(resp.mfa_token));
} else if (resp && resp.access_token && resp.organizations.length > 0) {
dispatch(setSubnetAccessToken(resp.access_token));
dispatch(setSubnetOrganizations(resp.organizations));
dispatch(
setSelectedSubnetOrganization(
resp.organizations[0].accountId.toString(),
),
);
}
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
dispatch(setLoading(false));
dispatch(resetRegisterForm());
});
},
);

View File

@@ -1,20 +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/>.
export interface ICallHomeResponse {
diagnosticsStatus?: boolean;
logsStatus?: boolean;
}

View File

@@ -1,130 +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 { CopyIcon, SettingsIcon, Box, Grid, Switch, InputBox } from "mds";
import RegistrationStatusBanner from "./RegistrationStatusBanner";
export const ClusterRegistered = ({ email }: { email: string }) => {
return (
<Fragment>
<RegistrationStatusBanner email={email} />
<Grid item xs={12} sx={{ marginTop: 25 }}>
<Box
sx={{
padding: "20px",
}}
>
Login to{" "}
<a href="https://subnet.min.io" target="_blank">
SUBNET
</a>{" "}
to avail support for this MinIO cluster
</Box>
</Grid>
</Fragment>
);
};
export const ProxyConfiguration = () => {
const proxyConfigurationCommand =
"mc admin config set {alias} subnet proxy={proxy}";
const [displaySubnetProxy, setDisplaySubnetProxy] = useState(false);
return (
<Fragment>
<Box
withBorders
sx={{
display: "flex",
padding: "23px",
marginTop: "40px",
alignItems: "start",
justifyContent: "space-between",
}}
>
<Box
sx={{
display: "flex",
flexFlow: "column",
}}
>
<Box
sx={{
display: "flex",
"& .min-icon": {
height: "22px",
width: "22px",
},
}}
>
<SettingsIcon />
<div style={{ marginLeft: "10px", fontWeight: 600 }}>
Proxy Configuration
</div>
</Box>
<Box
sx={{
marginTop: "10px",
marginBottom: "10px",
fontSize: "14px",
}}
>
For airgap/firewalled environments it is possible to{" "}
<a
href="https://min.io/docs/minio/linux/reference/minio-mc-admin/mc-admin-config.html?ref=con"
target="_blank"
>
configure a proxy
</a>{" "}
to connect to SUBNET .
</Box>
<Box>
{displaySubnetProxy && (
<InputBox
disabled
id="subnetProxy"
name="subnetProxy"
placeholder=""
onChange={() => {}}
label=""
value={proxyConfigurationCommand}
overlayIcon={<CopyIcon />}
overlayAction={() =>
navigator.clipboard.writeText(proxyConfigurationCommand)
}
/>
)}
</Box>
</Box>
<Box
sx={{
display: "flex",
}}
>
<Switch
value="enableProxy"
id="enableProxy"
name="enableProxy"
checked={displaySubnetProxy}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setDisplaySubnetProxy(event.target.checked);
}}
/>
</Box>
</Box>
</Fragment>
);
};

View File

@@ -1,465 +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 {
Box,
breakPoints,
Button,
FormLayout,
HelpBox,
InputBox,
InspectMenuIcon,
PageLayout,
PasswordKeyIcon,
Switch,
} from "mds";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import {
deleteCookie,
getCookieValue,
performDownload,
} from "../../../common/utils";
import {
selDistSet,
setErrorSnackMessage,
setHelpName,
} from "../../../systemSlice";
import { useAppDispatch } from "../../../store";
import { registeredCluster } from "../../../config";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import DistributedOnly from "../Common/DistributedOnly/DistributedOnly";
import KeyRevealer from "./KeyRevealer";
import RegisterCluster from "../Support/RegisterCluster";
import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper";
import HelpMenu from "../HelpMenu";
const ExampleBlock = ({
volumeVal,
pathVal,
}: {
volumeVal: string;
pathVal: string;
}) => {
return (
<Box className="code-block-container">
<Box className="example-code-block">
<Box
sx={{
display: "flex",
marginBottom: "5px",
flexFlow: "row",
[`@media (max-width: ${breakPoints.sm}px)`]: {
flexFlow: "column",
},
}}
>
<label>Volume/bucket Name :</label> <code>{volumeVal}</code>
</Box>
<Box
sx={{
display: "flex",
flexFlow: "row",
[`@media (max-width: ${breakPoints.sm}px)`]: {
flexFlow: "column",
},
}}
>
<label>Path : </label>
<code>{pathVal}</code>
</Box>
</Box>
</Box>
);
};
const Inspect = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const distributedSetup = useSelector(selDistSet);
const [volumeName, setVolumeName] = useState<string>("");
const [inspectPath, setInspectPath] = useState<string>("");
const [isEncrypt, setIsEncrypt] = useState<boolean>(true);
const [decryptionKey, setDecryptionKey] = useState<string>("");
const [insFileName, setInsFileName] = useState<string>("");
const [isFormValid, setIsFormValid] = useState<boolean>(false);
const [volumeError, setVolumeError] = useState<string>("");
const [pathError, setPathError] = useState<string>("");
const clusterRegistered = registeredCluster();
/**
* Validation Effect
*/
useEffect(() => {
let isVolValid;
let isPathValid;
isVolValid = volumeName.trim().length > 0;
if (!isVolValid) {
setVolumeError("This field is required");
} else if (volumeName.slice(0, 1) === "/") {
isVolValid = false;
setVolumeError("Volume/Bucket name cannot start with /");
}
isPathValid = inspectPath.trim().length > 0;
if (!inspectPath) {
setPathError("This field is required");
} else if (inspectPath.slice(0, 1) === "/") {
isPathValid = false;
setPathError("Path cannot start with /");
}
const isValid = isVolValid && isPathValid;
if (isVolValid) {
setVolumeError("");
}
if (isPathValid) {
setPathError("");
}
setIsFormValid(isValid);
}, [volumeName, inspectPath]);
const makeRequest = async (url: string) => {
return await fetch(url, { method: "GET" });
};
const performInspect = async () => {
let basename = document.baseURI.replace(window.location.origin, "");
const urlOfInspectApi = `${basename}/api/v1/admin/inspect?volume=${encodeURIComponent(volumeName)}&file=${encodeURIComponent(inspectPath)}&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);
setDecryptionKey(decryptKey);
})
.catch((err) => {
dispatch(setErrorSnackMessage(err));
});
};
const resetForm = () => {
setVolumeName("");
setInspectPath("");
setIsEncrypt(true);
};
const onCloseDecKeyModal = () => {
deleteCookie(insFileName);
setDecryptionKey("");
resetForm();
};
useEffect(() => {
dispatch(setHelpName("inspect"));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Fragment>
<PageHeaderWrapper label={"Inspect"} actions={<HelpMenu />} />
<PageLayout>
{!clusterRegistered && <RegisterCluster compactMode />}
{!distributedSetup ? (
<DistributedOnly
iconComponent={<InspectMenuIcon />}
entity={"Inspect"}
/>
) : (
<FormLayout
helpBox={
<HelpBox
title={"Learn more about the Inspect feature"}
iconComponent={<InspectMenuIcon />}
help={
<Fragment>
<Box
sx={{
marginTop: "16px",
fontWeight: 600,
fontStyle: "italic",
fontSize: "14px",
}}
>
Examples:
</Box>
<Box
sx={{
display: "flex",
flexFlow: "column",
fontSize: "14px",
flex: "2",
"& .step-row": {
fontSize: "14px",
display: "flex",
marginTop: "15px",
marginBottom: "15px",
"&.step-text": {
fontWeight: 400,
},
"&:before": {
content: "' '",
height: "7px",
width: "7px",
backgroundColor: "#2781B0",
marginRight: "10px",
marginTop: "7px",
flexShrink: 0,
},
},
"& .code-block-container": {
flex: "1",
marginTop: "15px",
marginLeft: "35px",
"& input": {
color: "#737373",
},
},
"& .example-code-block label": {
display: "inline-block",
width: 160,
fontWeight: 600,
fontSize: 14,
[`@media (max-width: ${breakPoints.sm}px)`]: {
width: "100%",
},
},
"& code": {
width: 100,
paddingLeft: "10px",
fontFamily: "monospace",
paddingRight: "10px",
paddingTop: "3px",
paddingBottom: "3px",
borderRadius: "2px",
border: "1px solid #eaeaea",
fontSize: "10px",
fontWeight: 500,
[`@media (max-width: ${breakPoints.sm}px)`]: {
width: "100%",
},
},
"& .spacer": {
marginBottom: "5px",
},
}}
>
<Box>
<Box className="step-row">
<div className="step-text">
To Download 'xl.meta' for a specific object from all
the drives in a zip file:
</div>
</Box>
<ExampleBlock
pathVal={`test*/xl.meta`}
volumeVal={`test-bucket`}
/>
</Box>
<Box>
<Box className="step-row">
<div className="step-text">
To Download all constituent parts for a specific
object, and optionally encrypt the downloaded zip:
</div>
</Box>
<ExampleBlock
pathVal={`test*/xl.meta`}
volumeVal={`test*/*/part.*`}
/>
</Box>
<Box>
<Box className="step-row">
<div className="step-text">
To Download recursively all objects at a prefix.
<br />
NOTE: This can be an expensive operation use it with
caution.
</div>
</Box>
<ExampleBlock
pathVal={`test*/xl.meta`}
volumeVal={`test/**`}
/>
</Box>
</Box>
<Box
sx={{
marginTop: "30px",
marginLeft: "15px",
fontSize: "14px",
}}
>
You can learn more at our{" "}
<a
href="https://github.com/minio/minio/tree/master/docs/debugging?ref=con"
target="_blank"
rel="noopener"
>
documentation
</a>
.
</Box>
</Fragment>
}
/>
}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!clusterRegistered) {
navigate("/support/register");
return;
}
performInspect();
}}
>
<InputBox
id="inspect_volume"
name="inspect_volume"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setVolumeName(e.target.value);
}}
label="Volume or Bucket Name"
value={volumeName}
error={volumeError}
required
placeholder={"test-bucket"}
disabled={!clusterRegistered}
/>
<InputBox
id="inspect_path"
name="inspect_path"
error={pathError}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setInspectPath(e.target.value);
}}
label="File or Path to inspect"
value={inspectPath}
required
placeholder={"test*/xl.meta"}
disabled={!clusterRegistered}
/>
<Switch
label="Encrypt"
indicatorLabels={["True", "False"]}
checked={isEncrypt}
value={"true"}
id="inspect_encrypt"
name="inspect_encrypt"
onChange={() => {
setIsEncrypt(!isEncrypt);
}}
disabled={!clusterRegistered}
/>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
marginTop: "55px",
}}
>
<Button
id={"inspect-clear-button"}
style={{
marginRight: "15px",
}}
type="button"
variant="regular"
data-test-id="inspect-clear-button"
onClick={resetForm}
label={"Clear"}
disabled={!clusterRegistered}
/>
<Button
id={"inspect-start"}
type="submit"
variant={!clusterRegistered ? "regular" : "callAction"}
data-test-id="inspect-submit-button"
disabled={!isFormValid || !clusterRegistered}
label={"Inspect"}
/>
</Box>
</form>
</FormLayout>
)}
{decryptionKey ? (
<ModalWrapper
modalOpen={true}
title="Inspect Decryption Key"
onClose={onCloseDecKeyModal}
titleIcon={<PasswordKeyIcon />}
>
<Fragment>
<Box>
This will be displayed only once. It cannot be recovered.
<br />
Use secure medium to share this key.
</Box>
<form
noValidate
onSubmit={() => {
return false;
}}
>
<KeyRevealer value={decryptionKey} />
</form>
</Fragment>
</ModalWrapper>
) : null}
</PageLayout>
</Fragment>
);
};
export default Inspect;

View File

@@ -1,40 +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 { Route, Routes } from "react-router-dom";
import withSuspense from "../Common/Components/withSuspense";
import NotFoundPage from "../../NotFoundPage";
import CallHome from "../Support/CallHome";
const Inspect = withSuspense(React.lazy(() => import("./Inspect")));
const Register = withSuspense(React.lazy(() => import("../Support/Register")));
const Profile = withSuspense(React.lazy(() => import("../Support/Profile")));
const Tools = () => {
return (
<Routes>
<Route path={"register"} element={<Register />} />
<Route path={"profile"} element={<Profile />} />
<Route path={"call-home"} element={<CallHome />} />
<Route path={"inspect"} element={<Inspect />} />
<Route path={"*"} element={<NotFoundPage />} />
</Routes>
);
};
export default Tools;

View File

@@ -1,502 +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 { Fragment, useEffect, useState } from "react";
import { DateTime } from "luxon";
import { useSelector } from "react-redux";
import {
Box,
breakPoints,
Button,
Checkbox,
DataTable,
FilterIcon,
Grid,
InputBox,
PageLayout,
} from "mds";
import { AppState, useAppDispatch } from "../../../store";
import { TraceMessage } from "./types";
import { niceBytes, timeFromDate } from "../../../common/utils";
import { wsProtocol } from "../../../utils/wsUtils";
import {
setTraceStarted,
traceMessageReceived,
traceResetMessages,
} from "./traceSlice";
import { setHelpName } from "../../../systemSlice";
import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper";
import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper";
import HelpMenu from "../HelpMenu";
import useWebSocket, { ReadyState } from "react-use-websocket";
const Trace = () => {
const dispatch = useAppDispatch();
const messages = useSelector((state: AppState) => state.trace.messages);
const traceStarted = useSelector(
(state: AppState) => state.trace.traceStarted,
);
const [statusCode, setStatusCode] = useState<string>("");
const [method, setMethod] = useState<string>("");
const [func, setFunc] = useState<string>("");
const [path, setPath] = useState<string>("");
const [threshold, setThreshold] = useState<number>(0);
const [all, setAll] = useState<boolean>(false);
const [s3, setS3] = useState<boolean>(true);
const [internal, setInternal] = useState<boolean>(false);
const [storage, setStorage] = useState<boolean>(false);
const [os, setOS] = useState<boolean>(false);
const [errors, setErrors] = useState<boolean>(false);
const [toggleFilter, setToggleFilter] = useState<boolean>(false);
const [logActive, setLogActive] = useState(false);
const [wsUrl, setWsUrl] = useState<string>("");
useEffect(() => {
const url = new URL(window.location.toString());
const wsProt = wsProtocol(url.protocol);
const port = process.env.NODE_ENV === "development" ? "9090" : url.port;
const calls = all
? "all"
: (() => {
const c = [];
if (s3) c.push("s3");
if (internal) c.push("internal");
if (storage) c.push("storage");
if (os) c.push("os");
return c.join(",");
})();
// check if we are using base path, if not this always is `/`
const baseLocation = new URL(document.baseURI).pathname;
const wsUrl = new URL(
`${wsProt}://${url.hostname}:${port}${baseLocation}ws/trace`,
);
wsUrl.searchParams.append("calls", calls);
wsUrl.searchParams.append("threshold", threshold.toString());
wsUrl.searchParams.append("onlyErrors", errors ? "yes" : "no");
wsUrl.searchParams.append("statusCode", statusCode);
wsUrl.searchParams.append("method", method);
wsUrl.searchParams.append("funcname", func);
wsUrl.searchParams.append("path", path);
setWsUrl(wsUrl.href);
}, [
all,
s3,
internal,
storage,
os,
threshold,
errors,
statusCode,
method,
func,
path,
]);
const { sendMessage, lastJsonMessage, readyState } =
useWebSocket<TraceMessage>(
wsUrl,
{
heartbeat: {
message: "ok",
interval: 10 * 1000, // send ok every 10 seconds
timeout: 365 * 24 * 60 * 60 * 1000, // disconnect after 365 days (workaround, because heartbeat gets no response)
},
},
logActive,
);
useEffect(() => {
if (readyState === ReadyState.CONNECTING) {
dispatch(traceResetMessages());
} else if (readyState === ReadyState.OPEN) {
dispatch(setTraceStarted(true));
} else if (readyState === ReadyState.CLOSED) {
dispatch(setTraceStarted(false));
}
}, [readyState, dispatch, sendMessage]);
useEffect(() => {
if (lastJsonMessage) {
lastJsonMessage.ptime = DateTime.fromISO(lastJsonMessage.time).toJSDate();
lastJsonMessage.key = Math.random();
dispatch(traceMessageReceived(lastJsonMessage));
}
}, [lastJsonMessage, dispatch]);
useEffect(() => {
dispatch(setHelpName("trace"));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Fragment>
<PageHeaderWrapper label={"Trace"} actions={<HelpMenu />} />
<PageLayout>
<Box withBorders>
<Grid container>
<Grid
item
xs={12}
sx={{
display: "flex",
flexFlow: "column",
"& .trace-Checkbox-label": {
fontSize: "14px",
fontWeight: "normal",
},
}}
>
<Box
sx={{
fontSize: "16px",
fontWeight: 600,
padding: "20px 0px 20px 0",
}}
>
Calls to Trace
</Box>
<Box
className={`${traceStarted ? "inactive-state" : ""}`}
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<Box
sx={{
display: "flex",
flexFlow: "row",
"& .trace-checked-icon": {
border: "1px solid red",
},
[`@media (min-width: ${breakPoints.md}px)`]: {
gap: 30,
},
}}
>
<Checkbox
checked={all}
id={"all_calls"}
name={"all_calls"}
label={"All"}
onChange={() => setAll(!all)}
value={"all"}
disabled={traceStarted}
/>
<Checkbox
checked={s3 || all}
id={"s3_calls"}
name={"s3_calls"}
label={"S3"}
onChange={() => setS3(!s3)}
value={"s3"}
disabled={all || traceStarted}
/>
<Checkbox
checked={internal || all}
id={"internal_calls"}
name={"internal_calls"}
label={"Internal"}
onChange={() => setInternal(!internal)}
value={"internal"}
disabled={all || traceStarted}
/>
<Checkbox
checked={storage || all}
id={"storage_calls"}
name={"storage_calls"}
label={"Storage"}
onChange={() => setStorage(!storage)}
value={"storage"}
disabled={all || traceStarted}
/>
<Checkbox
checked={os || all}
id={"os_calls"}
name={"os_calls"}
label={"OS"}
onChange={() => setOS(!os)}
value={"os"}
disabled={all || traceStarted}
/>
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "15px",
}}
>
<TooltipWrapper tooltip={"More filter options"}>
<Button
id={"filter-toggle"}
onClick={() => setToggleFilter(!toggleFilter)}
label={"Filters"}
icon={<FilterIcon />}
variant={"regular"}
className={"filters-toggle-button"}
style={{
width: "118px",
background: toggleFilter ? "rgba(8, 28, 66, 0.04)" : "",
}}
/>
</TooltipWrapper>
{!traceStarted && (
<Button
id={"start-trace"}
label={"Start"}
data-test-id={"trace-start-button"}
variant="callAction"
onClick={() => setLogActive(true)}
style={{
width: "118px",
}}
/>
)}
{traceStarted && (
<Button
id={"stop-trace"}
label={"Stop Trace"}
data-test-id={"trace-stop-button"}
variant="callAction"
onClick={() => setLogActive(false)}
style={{
width: "118px",
}}
/>
)}
</Box>
</Box>
</Grid>
{toggleFilter ? (
<Box
useBackground
className={`${traceStarted ? "inactive-state" : ""}`}
sx={{
marginTop: "25px",
display: "flex",
flexFlow: "column",
padding: "30px",
width: "100%",
"& .orient-vertical": {
flexFlow: "column",
"& label": {
marginBottom: "10px",
fontWeight: 600,
},
"& .inputRebase": {
width: "90%",
},
},
"& .trace-Checkbox-label": {
fontSize: "14px",
fontWeight: "normal",
},
}}
>
<Box
sx={{
display: "flex",
}}
>
<InputBox
className="orient-vertical"
id="trace-status-code"
name="trace-status-code"
label="Status Code"
placeholder="e.g. 503"
value={statusCode}
onChange={(e) => setStatusCode(e.target.value)}
disabled={traceStarted}
/>
<InputBox
className="orient-vertical"
id="trace-function-name"
name="trace-function-name"
label="Function Name"
placeholder="e.g. FunctionName2055"
value={func}
onChange={(e) => setFunc(e.target.value)}
disabled={traceStarted}
/>
<InputBox
className="orient-vertical"
id="trace-method"
name="trace-method"
label="Method"
placeholder="e.g. Method 2056"
value={method}
onChange={(e) => setMethod(e.target.value)}
disabled={traceStarted}
/>
</Box>
<Box
sx={{
gap: "30px",
display: "grid",
gridTemplateColumns: "2fr 1fr",
width: "100%",
marginTop: "33px",
}}
>
<Box
sx={{
flex: 2,
width: "calc( 100% + 10px)",
}}
>
<InputBox
className="orient-vertical"
id="trace-path"
name="trace-path"
label="Path"
placeholder="e.g. my-bucket/my-prefix/*"
value={path}
onChange={(e) => setPath(e.target.value)}
disabled={traceStarted}
/>
</Box>
<Box
sx={{
marginLeft: "15px",
}}
>
<InputBox
className="orient-vertical"
id="trace-fthreshold"
name="trace-fthreshold"
label="Response Threshold"
type="number"
placeholder="e.g. website.io.3249.114.12"
value={`${threshold}`}
onChange={(e) => setThreshold(parseInt(e.target.value))}
disabled={traceStarted}
/>
</Box>
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
marginTop: "40px",
}}
>
<Checkbox
checked={errors}
id={"only_errors"}
name={"only_errors"}
label={"Display only Errors"}
onChange={() => setErrors(!errors)}
value={"only_errors"}
disabled={traceStarted}
/>
</Box>
</Box>
) : null}
<Grid item xs={12}>
<Box
sx={{
fontSize: "16px",
fontWeight: 600,
marginBottom: "30px",
marginTop: "30px",
}}
>
Trace Results
</Box>
</Grid>
<Grid item xs={12}>
<DataTable
columns={[
{
label: "Time",
elementKey: "ptime",
renderFunction: (time: Date) => {
const timeParse = new Date(time);
return timeFromDate(timeParse);
},
width: 100,
},
{ label: "Name", elementKey: "api" },
{
label: "Status",
elementKey: "",
renderFunction: (fullElement: TraceMessage) =>
`${fullElement.statusCode} ${fullElement.statusMsg}`,
renderFullObject: true,
},
{
label: "Location",
elementKey: "configuration_id",
renderFunction: (fullElement: TraceMessage) =>
`${fullElement.host} ${fullElement.client}`,
renderFullObject: true,
},
{
label: "Load Time",
elementKey: "callStats.duration",
width: 150,
},
{
label: "Upload",
elementKey: "callStats.rx",
renderFunction: niceBytes,
width: 150,
},
{
label: "Download",
elementKey: "callStats.tx",
renderFunction: niceBytes,
width: 150,
},
]}
isLoading={false}
records={messages}
entityName="Traces"
idField="api"
customEmptyMessage={
traceStarted
? "No Traced elements received yet"
: "Trace is not started yet"
}
customPaperHeight={"calc(100vh - 292px)"}
autoScrollToBottom
/>
</Grid>
</Grid>
</Box>
</PageLayout>
</Fragment>
);
};
export default Trace;

View File

@@ -1,49 +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 { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { TraceMessage } from "./types";
interface TraceState {
messages: TraceMessage[];
traceStarted: boolean;
}
const initialState: TraceState = {
messages: [],
traceStarted: false,
};
const traceSlice = createSlice({
name: "trace",
initialState,
reducers: {
traceMessageReceived: (state, action: PayloadAction<TraceMessage>) => {
state.messages.push(action.payload);
},
traceResetMessages: (state) => {
state.messages = [];
},
setTraceStarted: (state, action: PayloadAction<boolean>) => {
state.traceStarted = action.payload;
},
},
});
export const { traceMessageReceived, traceResetMessages, setTraceStarted } =
traceSlice.actions;
export default traceSlice.reducer;

View File

@@ -1,36 +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/>.
interface CallStats {
timeToFirstByte: string;
rx: number;
tx: number;
duration: string;
}
export interface TraceMessage {
client: string;
time: string;
ptime: Date;
statusCode: number;
api: string;
query: string;
host: string;
callStats: CallStats;
path: string;
statusMsg: string;
key: number;
}

View File

@@ -1,234 +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 { useSelector } from "react-redux";
import {
Box,
Button,
DataTable,
Grid,
InputBox,
InputLabel,
PageLayout,
Select,
} from "mds";
import { AppState, useAppDispatch } from "../../../store";
import { Bucket, BucketList, EventInfo } from "./types";
import { niceBytes, timeFromDate } from "../../../common/utils";
import { wsProtocol } from "../../../utils/wsUtils";
import { ErrorResponseHandler } from "../../../common/types";
import { watchMessageReceived, watchResetMessages } from "./watchSlice";
import { setHelpName } from "../../../systemSlice";
import api from "../../../common/api";
import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper";
import HelpMenu from "../HelpMenu";
const Watch = () => {
const dispatch = useAppDispatch();
const messages = useSelector((state: AppState) => state.watch.messages);
const [start, setStart] = useState(false);
const [bucketName, setBucketName] = useState("Select Bucket");
const [prefix, setPrefix] = useState("");
const [suffix, setSuffix] = useState("");
const [bucketList, setBucketList] = useState<Bucket[]>([]);
const fetchBucketList = () => {
api
.invoke("GET", `/api/v1/buckets`)
.then((res: BucketList) => {
let buckets: Bucket[] = [];
if (res.buckets !== null) {
buckets = res.buckets;
}
setBucketList(buckets);
})
.catch((err: ErrorResponseHandler) => {
console.error(err);
});
};
useEffect(() => {
fetchBucketList();
}, []);
useEffect(() => {
dispatch(watchResetMessages());
// begin watch if bucketName in bucketList and start pressed
if (start && bucketList.some((bucket) => bucket.name === bucketName)) {
const url = new URL(window.location.toString());
const isDev = process.env.NODE_ENV === "development";
const port = isDev ? "9090" : url.port;
// check if we are using base path, if not this always is `/`
const baseLocation = new URL(document.baseURI);
const baseUrl = baseLocation.pathname;
const wsProt = wsProtocol(url.protocol);
const socket = new WebSocket(
`${wsProt}://${url.hostname}:${port}${baseUrl}ws/watch/${bucketName}?prefix=${prefix}&suffix=${suffix}`,
);
let interval: any | null = null;
if (socket !== null) {
socket.onopen = () => {
console.log("WebSocket Client Connected");
socket.send("ok");
interval = setInterval(() => {
socket.send("ok");
}, 10 * 1000);
};
socket.onmessage = (message: MessageEvent) => {
let m: EventInfo = JSON.parse(message.data.toString());
m.Time = new Date(m.Time.toString());
m.key = Math.random();
dispatch(watchMessageReceived(m));
};
socket.onclose = () => {
clearInterval(interval);
console.log("connection closed by server");
// reset start status
setStart(false);
};
return () => {
// close websocket on useEffect cleanup
socket.close(1000);
clearInterval(interval);
console.log("closing websockets");
};
}
} else {
// reset start status
setStart(false);
}
}, [dispatch, start, bucketList, bucketName, prefix, suffix]);
const bucketNames = bucketList.map((bucketName) => ({
label: bucketName.name,
value: bucketName.name,
}));
useEffect(() => {
dispatch(setHelpName("watch"));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const optionsArray = bucketNames.map((option) => ({
label: option.label,
value: option.value,
}));
return (
<Fragment>
<PageHeaderWrapper label="Watch" actions={<HelpMenu />} />
<PageLayout>
<Grid container>
<Grid
item
xs={12}
sx={{
display: "flex",
gap: 10,
marginBottom: 15,
alignItems: "center",
}}
>
<Box sx={{ flexGrow: 1 }}>
<InputLabel>Bucket</InputLabel>
<Select
id="bucket-name"
name="bucket-name"
value={bucketName}
onChange={(value) => {
setBucketName(value as string);
}}
disabled={start}
options={optionsArray}
placeholder={"Select Bucket"}
/>
</Box>
<Box sx={{ flexGrow: 1 }}>
<InputLabel>Prefix</InputLabel>
<InputBox
id="prefix-resource"
disabled={start}
onChange={(e) => {
setPrefix(e.target.value);
}}
/>
</Box>
<Box sx={{ flexGrow: 1 }}>
<InputLabel>Suffix</InputLabel>
<InputBox
id="suffix-resource"
disabled={start}
onChange={(e) => {
setSuffix(e.target.value);
}}
/>
</Box>
<Box sx={{ alignSelf: "flex-end", paddingBottom: 4 }}>
{start ? (
<Button
id={"stop-watch"}
type="submit"
variant="callAction"
onClick={() => setStart(false)}
label={"Stop"}
/>
) : (
<Button
id={"start-watch"}
type="submit"
variant="callAction"
onClick={() => setStart(true)}
label={"Start"}
/>
)}
</Box>
</Grid>
<Grid item xs={12}>
<DataTable
columns={[
{
label: "Time",
elementKey: "Time",
renderFunction: timeFromDate,
},
{
label: "Size",
elementKey: "Size",
renderFunction: niceBytes,
},
{ label: "Type", elementKey: "Type" },
{ label: "Path", elementKey: "Path" },
]}
records={messages}
entityName={"Watch"}
customEmptyMessage={"No Changes at this time"}
idField={"watch_table"}
isLoading={false}
customPaperHeight={"calc(100vh - 270px)"}
/>
</Grid>
</Grid>
</PageLayout>
</Fragment>
);
};
export default Watch;

View File

@@ -1,41 +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 EventInfo {
Time: Date;
Size: number;
UserMetadata: Map<string, string>;
Path: string;
Type: string;
Host: string;
Port: string;
UserAgent: string;
key: number;
}
export interface Bucket {
details: Details;
name: string;
}
interface Details {
tags: object;
}
export interface BucketList {
buckets: Bucket[];
total: number;
}

View File

@@ -1,43 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { EventInfo } from "./types";
interface WatchState {
messages: EventInfo[];
}
const initialState: WatchState = {
messages: [],
};
const watchSlice = createSlice({
name: "trace",
initialState,
reducers: {
watchMessageReceived: (state, action: PayloadAction<EventInfo>) => {
state.messages.push(action.payload);
},
watchResetMessages: (state) => {
state.messages = [];
},
},
});
export const { watchResetMessages, watchMessageReceived } = watchSlice.actions;
export default watchSlice.reducer;

View File

@@ -29,12 +29,9 @@ import {
AccountsMenuIcon,
AuditLogsMenuIcon,
BucketsMenuIcon,
CallHomeMenuIcon,
DocumentationIcon,
GroupsMenuIcon,
HealthMenuIcon,
IdentityMenuIcon,
InspectMenuIcon,
LambdaIcon,
LicenseIcon,
LockOpenIcon,
@@ -43,14 +40,10 @@ import {
MetricsMenuIcon,
MonitoringMenuIcon,
ObjectBrowserIcon,
PerformanceMenuIcon,
ProfileMenuIcon,
RecoverIcon,
SettingsIcon,
TiersIcon,
TraceMenuIcon,
UsersMenuIcon,
WatchIcon,
} from "mds";
import { hasPermission } from "../../common/SecureComponent";
import EncryptionIcon from "../../icons/SidebarMenus/EncryptionIcon";
@@ -203,18 +196,6 @@ export const validRoutes = (
path: IAM_PAGES.TOOLS_AUDITLOGS,
icon: <AuditLogsMenuIcon />,
},
{
name: "Trace",
id: "monitorTrace",
path: IAM_PAGES.TOOLS_TRACE,
icon: <TraceMenuIcon />,
},
{
name: "Watch",
id: "monitorWatch",
icon: <WatchIcon />,
path: IAM_PAGES.TOOLS_WATCH,
},
{
name: "Encryption",
id: "monitorEncryption",
@@ -261,7 +242,7 @@ export const validRoutes = (
icon: <SettingsIcon />,
},
{
group: "Subnet",
group: "Administrator",
path: IAM_PAGES.LICENSE,
name: "License",
id: "license",
@@ -269,41 +250,6 @@ export const validRoutes = (
badge: licenseNotification,
forceDisplay: true,
},
{
group: "Subnet",
name: "Health",
id: "diagnostics",
icon: <HealthMenuIcon />,
path: IAM_PAGES.TOOLS_DIAGNOSTICS,
},
{
group: "Subnet",
name: "Performance",
id: "performance",
icon: <PerformanceMenuIcon />,
path: IAM_PAGES.TOOLS_SPEEDTEST,
},
{
group: "Subnet",
name: "Profile",
id: "profile",
icon: <ProfileMenuIcon />,
path: IAM_PAGES.PROFILE,
},
{
group: "Subnet",
name: "Inspect",
id: "inspectObjects",
path: IAM_PAGES.SUPPORT_INSPECT,
icon: <InspectMenuIcon />,
},
{
group: "Subnet",
name: "Call Home",
id: "callhome",
icon: <CallHomeMenuIcon />,
path: IAM_PAGES.CALL_HOME,
},
];
return consoleMenus.reduce((acc: IMenuItem[], item) => {

View File

@@ -19,10 +19,7 @@ import { combineReducers, configureStore } from "@reduxjs/toolkit";
import systemReducer from "./systemSlice";
import loginReducer from "./screens/LoginPage/loginSlice";
import traceReducer from "./screens/Console/Trace/traceSlice";
import logReducer from "./screens/Console/Logs/logsSlice";
import healthInfoReducer from "./screens/Console/HealthInfo/healthInfoSlice";
import watchReducer from "./screens/Console/Watch/watchSlice";
import consoleReducer from "./screens/Console/consoleSlice";
import addBucketsReducer from "./screens/Console/Buckets/ListBuckets/AddBucket/addBucketsSlice";
import bucketDetailsReducer from "./screens/Console/Buckets/BucketDetails/bucketDetailsSlice";
@@ -30,7 +27,6 @@ import objectBrowserReducer from "./screens/Console/ObjectBrowser/objectBrowserS
import dashboardReducer from "./screens/Console/Dashboard/dashboardSlice";
import createUserReducer from "./screens/Console/Users/AddUsersSlice";
import licenseReducer from "./screens/Console/License/licenseSlice";
import registerReducer from "./screens/Console/Support/registerSlice";
import destinationSlice from "./screens/Console/EventDestinations/destinationsSlice";
import { objectBrowserWSMiddleware } from "./websockets/objectBrowserWSMiddleware";
@@ -39,16 +35,12 @@ let objectsWS: WebSocket;
const rootReducer = combineReducers({
system: systemReducer,
login: loginReducer,
trace: traceReducer,
logs: logReducer,
watch: watchReducer,
console: consoleReducer,
addBucket: addBucketsReducer,
bucketDetails: bucketDetailsReducer,
objectBrowser: objectBrowserReducer,
healthInfo: healthInfoReducer,
dashboard: dashboardReducer,
register: registerReducer,
createUser: createUserReducer,
license: licenseReducer,
destination: destinationSlice,

View File

@@ -196,7 +196,6 @@ export const {
setErrorSnackMessage,
setModalErrorSnackMessage,
setModalSnackMessage,
setServerDiagStat,
globalSetDistributedSetup,
setSiteReplicationInfo,
setOverrideStyles,

View File

@@ -14,11 +14,6 @@
// 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/>.
// Close codes for websockets defined in RFC 6455
export const WSCloseAbnormalClosure = 1006;
export const WSClosePolicyViolation = 1008;
export const WSCloseInternalServerErr = 1011;
export const wsProtocol = (protocol: string): string => {
let wsProtocol = "ws";
if (protocol === "https:") {