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:
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>
|
||||
nodes,
|
||||
<strong>{totalDrives}</strong> 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 <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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
{" "}
|
||||
<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>
|
||||
{" "}
|
||||
<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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 />}
|
||||
/>
|
||||
|
||||
<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: <strong>{finalRes.servers}</strong>
|
||||
</Grid>
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
sm={6}
|
||||
md={3}
|
||||
lg={2}
|
||||
className={"detailedItem"}
|
||||
>
|
||||
Drives: <strong>{finalRes.disks}</strong>
|
||||
</Grid>
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
sm={6}
|
||||
md={3}
|
||||
lg={2}
|
||||
className={"detailedItem"}
|
||||
>
|
||||
Concurrent: <strong>{finalRes.concurrent}</strong>
|
||||
</Grid>
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
sm={12}
|
||||
md={12}
|
||||
lg={5}
|
||||
className={"detailedVersion"}
|
||||
>
|
||||
<span className={"versionIcon"}>
|
||||
<VersionIcon />
|
||||
</span>{" "}
|
||||
MinIO VERSION <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))}
|
||||
|
||||
{stats.getUnit}/s.
|
||||
</td>
|
||||
</Fragment>
|
||||
)}
|
||||
{stats.putError && stats.putError !== "" ? (
|
||||
<td>{stats.putError}</td>
|
||||
) : (
|
||||
<Fragment>
|
||||
<td className={"serverValue"}>
|
||||
{prettyNumber(parseFloat(stats.putValue))}
|
||||
|
||||
{stats.putUnit}/s.
|
||||
</td>
|
||||
</Fragment>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Grid>
|
||||
</Fragment>
|
||||
)}
|
||||
</Grid>
|
||||
</Box>
|
||||
</Fragment>
|
||||
)}
|
||||
</STResultsContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default STResults;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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];
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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());
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -196,7 +196,6 @@ export const {
|
||||
setErrorSnackMessage,
|
||||
setModalErrorSnackMessage,
|
||||
setModalSnackMessage,
|
||||
setServerDiagStat,
|
||||
globalSetDistributedSetup,
|
||||
setSiteReplicationInfo,
|
||||
setOverrideStyles,
|
||||
|
||||
@@ -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:") {
|
||||
|
||||
Reference in New Issue
Block a user