diff --git a/Dockerfile b/Dockerfile
index c5321e662..315c950d6 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -29,7 +29,7 @@ WORKDIR /go/src/github.com/minio/console/
ENV CGO_ENABLED=0
COPY --from=uilayer /app/build /go/src/github.com/minio/console/portal-ui/build
-RUN go build -ldflags "-w -s" -a -o console ./cmd/console
+RUN go build --tags=kqueue,operator -ldflags "-w -s" -a -o console ./cmd/console
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.5
MAINTAINER MinIO Development "dev@min.io"
diff --git a/models/login_request.go b/models/login_request.go
index efd9eaa25..85793e977 100644
--- a/models/login_request.go
+++ b/models/login_request.go
@@ -40,6 +40,9 @@ type LoginRequest struct {
// Required: true
AccessKey *string `json:"accessKey"`
+ // features
+ Features *LoginRequestFeatures `json:"features,omitempty"`
+
// secret key
// Required: true
SecretKey *string `json:"secretKey"`
@@ -53,6 +56,10 @@ func (m *LoginRequest) Validate(formats strfmt.Registry) error {
res = append(res, err)
}
+ if err := m.validateFeatures(formats); err != nil {
+ res = append(res, err)
+ }
+
if err := m.validateSecretKey(formats); err != nil {
res = append(res, err)
}
@@ -72,6 +79,25 @@ func (m *LoginRequest) validateAccessKey(formats strfmt.Registry) error {
return nil
}
+func (m *LoginRequest) validateFeatures(formats strfmt.Registry) error {
+ if swag.IsZero(m.Features) { // not required
+ return nil
+ }
+
+ if m.Features != nil {
+ if err := m.Features.Validate(formats); err != nil {
+ if ve, ok := err.(*errors.Validation); ok {
+ return ve.ValidateName("features")
+ } else if ce, ok := err.(*errors.CompositeError); ok {
+ return ce.ValidateName("features")
+ }
+ return err
+ }
+ }
+
+ return nil
+}
+
func (m *LoginRequest) validateSecretKey(formats strfmt.Registry) error {
if err := validate.Required("secretKey", "body", m.SecretKey); err != nil {
@@ -81,8 +107,33 @@ func (m *LoginRequest) validateSecretKey(formats strfmt.Registry) error {
return nil
}
-// ContextValidate validates this login request based on context it is used
+// ContextValidate validate this login request based on the context it is used
func (m *LoginRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
+ var res []error
+
+ if err := m.contextValidateFeatures(ctx, formats); err != nil {
+ res = append(res, err)
+ }
+
+ if len(res) > 0 {
+ return errors.CompositeValidationError(res...)
+ }
+ return nil
+}
+
+func (m *LoginRequest) contextValidateFeatures(ctx context.Context, formats strfmt.Registry) error {
+
+ if m.Features != nil {
+ if err := m.Features.ContextValidate(ctx, formats); err != nil {
+ if ve, ok := err.(*errors.Validation); ok {
+ return ve.ValidateName("features")
+ } else if ce, ok := err.(*errors.CompositeError); ok {
+ return ce.ValidateName("features")
+ }
+ return err
+ }
+ }
+
return nil
}
@@ -103,3 +154,40 @@ func (m *LoginRequest) UnmarshalBinary(b []byte) error {
*m = res
return nil
}
+
+// LoginRequestFeatures login request features
+//
+// swagger:model LoginRequestFeatures
+type LoginRequestFeatures struct {
+
+ // hide menu
+ HideMenu bool `json:"hide_menu,omitempty"`
+}
+
+// Validate validates this login request features
+func (m *LoginRequestFeatures) Validate(formats strfmt.Registry) error {
+ return nil
+}
+
+// ContextValidate validates this login request features based on context it is used
+func (m *LoginRequestFeatures) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
+ return nil
+}
+
+// MarshalBinary interface implementation
+func (m *LoginRequestFeatures) MarshalBinary() ([]byte, error) {
+ if m == nil {
+ return nil, nil
+ }
+ return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *LoginRequestFeatures) UnmarshalBinary(b []byte) error {
+ var res LoginRequestFeatures
+ if err := swag.ReadJSON(b, &res); err != nil {
+ return err
+ }
+ *m = res
+ return nil
+}
diff --git a/models/principal.go b/models/principal.go
index d9b2c1687..231e24d23 100644
--- a/models/principal.go
+++ b/models/principal.go
@@ -45,6 +45,9 @@ type Principal struct {
// account access key
AccountAccessKey string `json:"accountAccessKey,omitempty"`
+
+ // Hide Console Menu
+ Hm bool `json:"hm,omitempty"`
}
// Validate validates this principal
diff --git a/operatorapi/embedded_spec.go b/operatorapi/embedded_spec.go
index ce03ce2ea..6f65909ee 100644
--- a/operatorapi/embedded_spec.go
+++ b/operatorapi/embedded_spec.go
@@ -2605,6 +2605,14 @@ func init() {
"accessKey": {
"type": "string"
},
+ "features": {
+ "type": "object",
+ "properties": {
+ "hide_menu": {
+ "type": "boolean"
+ }
+ }
+ },
"secretKey": {
"type": "string"
}
@@ -5812,6 +5820,14 @@ func init() {
}
}
},
+ "LoginRequestFeatures": {
+ "type": "object",
+ "properties": {
+ "hide_menu": {
+ "type": "boolean"
+ }
+ }
+ },
"NodeSelectorTermMatchExpressionsItems0": {
"description": "A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values.",
"type": "object",
@@ -7043,6 +7059,14 @@ func init() {
"accessKey": {
"type": "string"
},
+ "features": {
+ "type": "object",
+ "properties": {
+ "hide_menu": {
+ "type": "boolean"
+ }
+ }
+ },
"secretKey": {
"type": "string"
}
diff --git a/operatorapi/operator_login.go b/operatorapi/operator_login.go
index 413df05d0..25fc5bc0d 100644
--- a/operatorapi/operator_login.go
+++ b/operatorapi/operator_login.go
@@ -83,7 +83,7 @@ func login(credentials restapi.ConsoleCredentialsI) (*string, error) {
return nil, err
}
// if we made it here, the consoleCredentials work, generate a jwt with claims
- token, err := auth.NewEncryptedTokenForClient(&tokens, credentials.GetAccountAccessKey())
+ token, err := auth.NewEncryptedTokenForClient(&tokens, credentials.GetAccountAccessKey(), nil)
if err != nil {
LogError("error authenticating user: %v", err)
return nil, errInvalidCredentials
diff --git a/operatorapi/proxy.go b/operatorapi/proxy.go
index 835a9dc89..80dbd50cf 100644
--- a/operatorapi/proxy.go
+++ b/operatorapi/proxy.go
@@ -123,9 +123,12 @@ func serveProxy(responseWriter http.ResponseWriter, req *http.Request) {
return
}
- data := map[string]string{
- "accessKey": string(tenantConfiguration["accesskey"]),
- "secretKey": string(tenantConfiguration["secretkey"]),
+ data := map[string]interface{}{
+ "accessKey": tenantConfiguration["accesskey"],
+ "secretKey": tenantConfiguration["secretkey"],
+ "features": map[string]bool{
+ "hide_menu": true,
+ },
}
payload, _ := json.Marshal(data)
diff --git a/pkg/auth/token.go b/pkg/auth/token.go
index 1a9baf58c..1cda95da1 100644
--- a/pkg/auth/token.go
+++ b/pkg/auth/token.go
@@ -66,6 +66,12 @@ type TokenClaims struct {
STSSecretAccessKey string `json:"stsSecretAccessKey,omitempty"`
STSSessionToken string `json:"stsSessionToken,omitempty"`
AccountAccessKey string `json:"accountAccessKey,omitempty"`
+ HideMenu bool `json:"hm,omitempty"`
+}
+
+// SessionFeatures represents features stored in the session
+type SessionFeatures struct {
+ HideMenu bool
}
// SessionTokenAuthenticate takes a session token, decode it, extract claims and validate the signature
@@ -96,14 +102,18 @@ func SessionTokenAuthenticate(token string) (*TokenClaims, error) {
// NewEncryptedTokenForClient generates a new session token with claims based on the provided STS credentials, first
// encrypts the claims and the sign them
-func NewEncryptedTokenForClient(credentials *credentials.Value, accountAccessKey string) (string, error) {
+func NewEncryptedTokenForClient(credentials *credentials.Value, accountAccessKey string, features *SessionFeatures) (string, error) {
if credentials != nil {
- encryptedClaims, err := encryptClaims(&TokenClaims{
+ tokenClaims := &TokenClaims{
STSAccessKeyID: credentials.AccessKeyID,
STSSecretAccessKey: credentials.SecretAccessKey,
STSSessionToken: credentials.SessionToken,
AccountAccessKey: accountAccessKey,
- })
+ }
+ if features != nil {
+ tokenClaims.HideMenu = features.HideMenu
+ }
+ encryptedClaims, err := encryptClaims(tokenClaims)
if err != nil {
return "", err
}
diff --git a/pkg/auth/token_test.go b/pkg/auth/token_test.go
index d0263042c..58cd077bd 100644
--- a/pkg/auth/token_test.go
+++ b/pkg/auth/token_test.go
@@ -36,14 +36,14 @@ func TestNewJWTWithClaimsForClient(t *testing.T) {
funcAssert := assert.New(t)
// Test-1 : NewEncryptedTokenForClient() is generated correctly without errors
function := "NewEncryptedTokenForClient()"
- token, err := NewEncryptedTokenForClient(creds, "")
+ token, err := NewEncryptedTokenForClient(creds, "", nil)
if err != nil || token == "" {
t.Errorf("Failed on %s:, error occurred: %s", function, err)
}
// saving token for future tests
goodToken = token
// Test-2 : NewEncryptedTokenForClient() throws error because of empty credentials
- if _, err = NewEncryptedTokenForClient(nil, ""); err != nil {
+ if _, err = NewEncryptedTokenForClient(nil, "", nil); err != nil {
funcAssert.Equal("provided credentials are empty", err.Error())
}
}
diff --git a/portal-ui/src/common/SecureComponent/permissions.ts b/portal-ui/src/common/SecureComponent/permissions.ts
index 8b2254a11..63986fcf9 100644
--- a/portal-ui/src/common/SecureComponent/permissions.ts
+++ b/portal-ui/src/common/SecureComponent/permissions.ts
@@ -129,8 +129,7 @@ export const IAM_PAGES = {
TOOLS_LOGS: "/tools/logs",
TOOLS_AUDITLOGS: "/tools/audit-logs",
TOOLS_TRACE: "/tools/trace",
- METRICS: "/tools/metrics",
- DASHBOARD: "/tools/dashboard",
+ DASHBOARD: "/tools/metrics",
TOOLS_HEAL: "/tools/heal",
TOOLS_WATCH: "/tools/watch",
/* Health */
@@ -178,6 +177,8 @@ export const IAM_PAGES = {
"/namespaces/:tenantNamespace/tenants/:tenantName/summary",
NAMESPACE_TENANT_METRICS:
"/namespaces/:tenantNamespace/tenants/:tenantName/metrics",
+ NAMESPACE_TENANT_TRACE:
+ "/namespaces/:tenantNamespace/tenants/:tenantName/trace",
NAMESPACE_TENANT_POOLS:
"/namespaces/:tenantNamespace/tenants/:tenantName/pools",
NAMESPACE_TENANT_VOLUMES:
@@ -297,9 +298,6 @@ export const IAM_PAGES_PERMISSIONS = {
[IAM_PAGES.DASHBOARD]: [
IAM_SCOPES.ADMIN_SERVER_INFO, // displays dashboard information
],
- [IAM_PAGES.METRICS]: [
- IAM_SCOPES.ADMIN_SERVER_INFO, // displays dashboard information
- ],
[IAM_PAGES.POLICIES_VIEW]: [
IAM_SCOPES.ADMIN_DELETE_POLICY,
IAM_SCOPES.ADMIN_LIST_GROUPS,
diff --git a/portal-ui/src/screens/Console/Common/PageHeader/PageHeader.tsx b/portal-ui/src/screens/Console/Common/PageHeader/PageHeader.tsx
index e8f3a1840..099df0175 100644
--- a/portal-ui/src/screens/Console/Common/PageHeader/PageHeader.tsx
+++ b/portal-ui/src/screens/Console/Common/PageHeader/PageHeader.tsx
@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-import React from "react";
+import React, { Fragment } from "react";
import { Theme } from "@mui/material/styles";
import { connect } from "react-redux";
import Grid from "@mui/material/Grid";
@@ -38,6 +38,7 @@ interface IPageHeader {
managerObjects?: IFileItem[];
toggleList: typeof toggleList;
middleComponent?: React.ReactNode;
+ features: string[];
}
const styles = (theme: Theme) =>
@@ -88,7 +89,11 @@ const PageHeader = ({
managerObjects,
toggleList,
middleComponent,
+ features,
}: IPageHeader) => {
+ if (features.includes("hide-menu")) {
+ return ;
+ }
return (
({
sidebarOpen: state.system.sidebarOpen,
operatorMode: state.system.operatorMode,
managerObjects: state.objectBrowser.objectManager.objectsToManage,
+ features: state.console.session.features,
});
const mapDispatchToProps = {
diff --git a/portal-ui/src/screens/Console/Console.tsx b/portal-ui/src/screens/Console/Console.tsx
index 81803e8bc..4dcd35e83 100644
--- a/portal-ui/src/screens/Console/Console.tsx
+++ b/portal-ui/src/screens/Console/Console.tsx
@@ -56,7 +56,6 @@ const Heal = React.lazy(() => import("./Heal/Heal"));
const Watch = React.lazy(() => import("./Watch/Watch"));
const HealthInfo = React.lazy(() => import("./HealthInfo/HealthInfo"));
const Storage = React.lazy(() => import("./Storage/Storage"));
-const Metrics = React.lazy(() => import("./Dashboard/Metrics"));
const Hop = React.lazy(() => import("./Tenants/TenantDetails/hop/Hop"));
const AddTenant = React.lazy(() => import("./Tenants/AddTenant/AddTenant"));
@@ -211,10 +210,6 @@ const Console = ({
component: Dashboard,
path: IAM_PAGES.DASHBOARD,
},
- {
- component: Metrics,
- path: IAM_PAGES.METRICS,
- },
{
component: Buckets,
path: IAM_PAGES.ADD_BUCKETS,
@@ -433,6 +428,11 @@ const Console = ({
path: IAM_PAGES.NAMESPACE_TENANT_METRICS,
forceDisplay: true,
},
+ {
+ component: TenantDetails,
+ path: IAM_PAGES.NAMESPACE_TENANT_TRACE,
+ forceDisplay: true,
+ },
{
component: TenantDetails,
path: IAM_PAGES.NAMESPACE_TENANT_PODS_LIST,
@@ -513,7 +513,7 @@ const Console = ({
const location = useLocation();
let hideMenu = false;
- if (location.pathname === IAM_PAGES.METRICS) {
+ if (features?.includes("hide-menu")) {
hideMenu = true;
} else if (location.pathname.endsWith("/hop")) {
hideMenu = true;
diff --git a/portal-ui/src/screens/Console/ConsoleKBar.tsx b/portal-ui/src/screens/Console/ConsoleKBar.tsx
index 6e3745d96..51db80ac1 100644
--- a/portal-ui/src/screens/Console/ConsoleKBar.tsx
+++ b/portal-ui/src/screens/Console/ConsoleKBar.tsx
@@ -84,6 +84,10 @@ const ConsoleKBar = ({
operatorMode: boolean;
features: string[] | null;
}) => {
+ if (features?.includes("hide-menu")) {
+ return ;
+ }
+
const allowedMenuItems = validRoutes(features, operatorMode);
const initialActions = [];
diff --git a/portal-ui/src/screens/Console/Dashboard/Metrics.tsx b/portal-ui/src/screens/Console/Dashboard/Metrics.tsx
deleted file mode 100644
index 39c1809fd..000000000
--- a/portal-ui/src/screens/Console/Dashboard/Metrics.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-// This file is part of MinIO Console Server
-// Copyright (c) 2021 MinIO, Inc.
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-import React, { Fragment, useCallback, useEffect, useState } from "react";
-import { connect } from "react-redux";
-import get from "lodash/get";
-import Grid from "@mui/material/Grid";
-import { Theme } from "@mui/material/styles";
-import createStyles from "@mui/styles/createStyles";
-import withStyles from "@mui/styles/withStyles";
-import { LinearProgress } from "@mui/material";
-import api from "../../../common/api";
-import { Usage } from "./types";
-import { setErrorSnackMessage } from "../../../actions";
-import { ErrorResponseHandler } from "../../../common/types";
-import PrDashboard from "./Prometheus/PrDashboard";
-import BasicDashboard from "./BasicDashboard/BasicDashboard";
-
-interface IMetricsSimple {
- classes: any;
- displayErrorMessage: typeof setErrorSnackMessage;
-}
-
-const styles = (theme: Theme) => createStyles({});
-
-const Metrics = ({ classes, displayErrorMessage }: IMetricsSimple) => {
- const [loading, setLoading] = useState(true);
- const [basicResult, setBasicResult] = useState(null);
-
- const fetchUsage = useCallback(() => {
- api
- .invoke("GET", `/api/v1/admin/info`)
- .then((res: Usage) => {
- setBasicResult(res);
- setLoading(false);
- })
- .catch((err: ErrorResponseHandler) => {
- displayErrorMessage(err);
- setLoading(false);
- });
- }, [setBasicResult, setLoading, displayErrorMessage]);
-
- useEffect(() => {
- if (loading) {
- fetchUsage();
- }
- }, [loading, fetchUsage]);
-
- const widgets = get(basicResult, "widgets", null);
-
- return (
-
-
- {loading ? (
-
-
-
- ) : (
-
- {widgets !== null ? (
-
-
-
- ) : (
-
- )}
-
- )}
-
-
- );
-};
-
-const connector = connect(null, {
- displayErrorMessage: setErrorSnackMessage,
-});
-
-export default withStyles(styles)(connector(Metrics));
diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantDetails.tsx b/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantDetails.tsx
index b36c5ac50..ab2307a3c 100644
--- a/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantDetails.tsx
+++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantDetails.tsx
@@ -50,7 +50,6 @@ import BackLink from "../../../../common/BackLink";
import VerticalTabs from "../../Common/VerticalTabs/VerticalTabs";
import BoxIconButton from "../../Common/BoxIconButton/BoxIconButton";
import withSuspense from "../../Common/Components/withSuspense";
-import PVCDetails from "./pvcs/PVCDetails";
const TenantYAML = withSuspense(React.lazy(() => import("./TenantYAML")));
const TenantSummary = withSuspense(React.lazy(() => import("./TenantSummary")));
@@ -63,6 +62,10 @@ const VolumesSummary = withSuspense(
React.lazy(() => import("./VolumesSummary"))
);
const TenantMetrics = withSuspense(React.lazy(() => import("./TenantMetrics")));
+const TenantTrace = withSuspense(React.lazy(() => import("./TenantTrace")));
+const TenantVolumes = withSuspense(
+ React.lazy(() => import("./pvcs/TenantVolumes"))
+);
const TenantSecurity = withSuspense(
React.lazy(() => import("./TenantSecurity"))
);
@@ -424,6 +427,10 @@ const TenantDetails = ({
path="/namespaces/:tenantNamespace/tenants/:tenantName/metrics"
component={TenantMetrics}
/>
+
{