diff --git a/pkg/obsservice/obslib/httpproxy/reverseproxy.go b/pkg/obsservice/obslib/httpproxy/reverseproxy.go index 786b01cd98a6..5c6560c92f3c 100644 --- a/pkg/obsservice/obslib/httpproxy/reverseproxy.go +++ b/pkg/obsservice/obslib/httpproxy/reverseproxy.go @@ -143,6 +143,8 @@ func (p *ReverseHTTPProxy) Start(ctx context.Context, stop *stop.Stopper) { OIDC: &noOIDCConfigured{}, Flags: serverpb.FeatureFlags{ IsObservabilityService: true, + // TODO(obs-infra): make conditional once obsservice becomes tenant-aware. + CanViewKvMetricDashboards: true, }, })) for _, path := range CRDBProxyPaths { diff --git a/pkg/server/server.go b/pkg/server/server.go index cdd2dc917cd0..1d9da8dd8c39 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -1862,6 +1862,9 @@ func (s *Server) PreStart(ctx context.Context) error { sqlServer: s.sqlServer, db: s.db, }), /* apiServer */ + serverpb.FeatureFlags{ + CanViewKvMetricDashboards: s.rpcContext.TenantID.Equal(roachpb.SystemTenantID), + }, /* flags */ ); err != nil { return err } diff --git a/pkg/server/server_http.go b/pkg/server/server_http.go index 1713c1b686f7..efd0f6c5af99 100644 --- a/pkg/server/server_http.go +++ b/pkg/server/server_http.go @@ -19,6 +19,7 @@ import ( "github.com/cockroachdb/cmux" "github.com/cockroachdb/cockroach/pkg/rpc" "github.com/cockroachdb/cockroach/pkg/server/debug" + "github.com/cockroachdb/cockroach/pkg/server/serverpb" "github.com/cockroachdb/cockroach/pkg/server/status" "github.com/cockroachdb/cockroach/pkg/settings" "github.com/cockroachdb/cockroach/pkg/ts" @@ -94,6 +95,7 @@ func (s *httpServer) setupRoutes( handleRequestsUnauthenticated http.Handler, handleDebugUnauthenticated http.Handler, apiServer http.Handler, + flags serverpb.FeatureFlags, ) error { // OIDC Configuration must happen prior to the UI Handler being defined below so that we have // the system settings initialized for it to pick up from the oidcAuthenticationServer. @@ -117,6 +119,7 @@ func (s *httpServer) setupRoutes( } return nil }, + Flags: flags, }) // The authentication mux used here is created in "allow anonymous" mode so that the UI diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index d3bda2ab0748..e0666ad4272b 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -804,7 +804,7 @@ Binary built without web UI. respBytes, err = io.ReadAll(resp.Body) require.NoError(t, err) expected := fmt.Sprintf( - `{"Insecure":true,"LoggedInUser":null,"Tag":"%s","Version":"%s","NodeID":"%d","OIDCAutoLogin":false,"OIDCLoginEnabled":false,"OIDCButtonText":"","FeatureFlags":{}}`, + `{"Insecure":true,"LoggedInUser":null,"Tag":"%s","Version":"%s","NodeID":"%d","OIDCAutoLogin":false,"OIDCLoginEnabled":false,"OIDCButtonText":"","FeatureFlags":{"can_view_kv_metric_dashboards":true}}`, build.GetInfo().Tag, build.BinaryVersionPrefix(), 1, @@ -832,7 +832,7 @@ Binary built without web UI. { loggedInClient, fmt.Sprintf( - `{"Insecure":false,"LoggedInUser":"authentic_user","Tag":"%s","Version":"%s","NodeID":"%d","OIDCAutoLogin":false,"OIDCLoginEnabled":false,"OIDCButtonText":"","FeatureFlags":{}}`, + `{"Insecure":false,"LoggedInUser":"authentic_user","Tag":"%s","Version":"%s","NodeID":"%d","OIDCAutoLogin":false,"OIDCLoginEnabled":false,"OIDCButtonText":"","FeatureFlags":{"can_view_kv_metric_dashboards":true}}`, build.GetInfo().Tag, build.BinaryVersionPrefix(), 1, @@ -841,7 +841,7 @@ Binary built without web UI. { loggedOutClient, fmt.Sprintf( - `{"Insecure":false,"LoggedInUser":null,"Tag":"%s","Version":"%s","NodeID":"%d","OIDCAutoLogin":false,"OIDCLoginEnabled":false,"OIDCButtonText":"","FeatureFlags":{}}`, + `{"Insecure":false,"LoggedInUser":null,"Tag":"%s","Version":"%s","NodeID":"%d","OIDCAutoLogin":false,"OIDCLoginEnabled":false,"OIDCButtonText":"","FeatureFlags":{"can_view_kv_metric_dashboards":true}}`, build.GetInfo().Tag, build.BinaryVersionPrefix(), 1, diff --git a/pkg/server/serverpb/admin.proto b/pkg/server/serverpb/admin.proto index ed2ce38587f3..9f6e52658c77 100644 --- a/pkg/server/serverpb/admin.proto +++ b/pkg/server/serverpb/admin.proto @@ -1457,4 +1457,6 @@ message SetTraceRecordingTypeResponse{} message FeatureFlags { // Whether the server is an instance of the Observability Service bool is_observability_service = 1; + // Whether the logged in user is able to view KV-level metric dashboards. + bool can_view_kv_metric_dashboards = 2; } diff --git a/pkg/server/tenant.go b/pkg/server/tenant.go index 19a445b933ee..501988b882f0 100644 --- a/pkg/server/tenant.go +++ b/pkg/server/tenant.go @@ -687,6 +687,9 @@ func (s *SQLServerWrapper) PreStart(ctx context.Context) error { sqlServer: s.sqlServer, db: s.db, }), /* apiServer */ + serverpb.FeatureFlags{ + CanViewKvMetricDashboards: s.rpcContext.TenantID.Equal(roachpb.SystemTenantID), + }, ); err != nil { return err } diff --git a/pkg/ui/workspaces/cluster-ui/src/util/dataFromServer.ts b/pkg/ui/workspaces/cluster-ui/src/util/dataFromServer.ts index 6c7f7b038011..c4581ae508e3 100644 --- a/pkg/ui/workspaces/cluster-ui/src/util/dataFromServer.ts +++ b/pkg/ui/workspaces/cluster-ui/src/util/dataFromServer.ts @@ -9,7 +9,7 @@ // licenses/APL.txt. import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; -import IFeatureFlags = cockroach.server.serverpb.IFeatureFlags; +import FeatureFlags = cockroach.server.serverpb.FeatureFlags; export interface DataFromServer { Insecure: boolean; @@ -20,7 +20,7 @@ export interface DataFromServer { OIDCAutoLogin: boolean; OIDCLoginEnabled: boolean; OIDCButtonText: string; - FeatureFlags: IFeatureFlags; + FeatureFlags: FeatureFlags; } // Tell TypeScript about `window.dataFromServer`, which is set in a script diff --git a/pkg/ui/workspaces/db-console/src/redux/state.ts b/pkg/ui/workspaces/db-console/src/redux/state.ts index fbe15cabe4f5..d492a567b216 100644 --- a/pkg/ui/workspaces/db-console/src/redux/state.ts +++ b/pkg/ui/workspaces/db-console/src/redux/state.ts @@ -37,6 +37,8 @@ import { loginReducer, LoginAPIState } from "./login"; import rootSaga from "./sagas"; import { initializeAnalytics } from "./analytics"; import { DataFromServer } from "src/util/dataFromServer"; +import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; +import FeatureFlags = cockroach.server.serverpb.FeatureFlags; export interface AdminUIState { cachedData: APIReducersState; @@ -53,7 +55,7 @@ export interface AdminUIState { const emptyDataFromServer: DataFromServer = { Insecure: true, - FeatureFlags: {}, + FeatureFlags: new FeatureFlags(), LoggedInUser: "", NodeID: "", OIDCAutoLogin: false, diff --git a/pkg/ui/workspaces/db-console/src/util/dataFromServer.ts b/pkg/ui/workspaces/db-console/src/util/dataFromServer.ts index 9a6e826db675..c9ca0c2bb307 100644 --- a/pkg/ui/workspaces/db-console/src/util/dataFromServer.ts +++ b/pkg/ui/workspaces/db-console/src/util/dataFromServer.ts @@ -9,7 +9,7 @@ // licenses/APL.txt. import { cockroach } from "@cockroachlabs/crdb-protobuf-client"; -import IFeatureFlags = cockroach.server.serverpb.IFeatureFlags; +import FeatureFlags = cockroach.server.serverpb.FeatureFlags; export interface DataFromServer { Insecure: boolean; @@ -20,7 +20,7 @@ export interface DataFromServer { OIDCAutoLogin: boolean; OIDCLoginEnabled: boolean; OIDCButtonText: string; - FeatureFlags: IFeatureFlags; + FeatureFlags: FeatureFlags; } // Tell TypeScript about `window.dataFromServer`, which is set in a script // tag in index.html, the contents of which are generated in a Go template diff --git a/pkg/ui/workspaces/db-console/src/views/cluster/components/linegraph/index.tsx b/pkg/ui/workspaces/db-console/src/views/cluster/components/linegraph/index.tsx index 770ccb89372a..d0db7ef15182 100644 --- a/pkg/ui/workspaces/db-console/src/views/cluster/components/linegraph/index.tsx +++ b/pkg/ui/workspaces/db-console/src/views/cluster/components/linegraph/index.tsx @@ -47,6 +47,7 @@ import { import _ from "lodash"; export interface LineGraphProps extends MetricsDataComponentProps { + isKvGraph?: boolean; title?: string; subtitle?: string; legend?: boolean; @@ -159,6 +160,11 @@ export class LineGraph extends React.Component { this.setNewTimeRange = this.setNewTimeRange.bind(this); } + static defaultProps: Partial = { + // Marking a graph as not being KV-related is opt-in. + isKvGraph: true, + }; + // axis is copied from the nvd3 LineGraph component above axis = createSelector( (props: { children?: React.ReactNode }) => props.children, diff --git a/pkg/ui/workspaces/db-console/src/views/cluster/containers/nodeGraphs/dashboards/changefeeds.tsx b/pkg/ui/workspaces/db-console/src/views/cluster/containers/nodeGraphs/dashboards/changefeeds.tsx index 36f2ffe646d7..e79393d1727c 100644 --- a/pkg/ui/workspaces/db-console/src/views/cluster/containers/nodeGraphs/dashboards/changefeeds.tsx +++ b/pkg/ui/workspaces/db-console/src/views/cluster/containers/nodeGraphs/dashboards/changefeeds.tsx @@ -20,7 +20,11 @@ export default function (props: GraphDashboardProps) { const { storeSources } = props; return [ - + , - + , - + , - + , - + Over the last minute, this node executed 99% of SQL statements within @@ -92,6 +94,7 @@ export default function (props: GraphDashboardProps) { , } preCalcGraphSize={true} diff --git a/pkg/ui/workspaces/db-console/src/views/cluster/containers/nodeGraphs/dashboards/sql.tsx b/pkg/ui/workspaces/db-console/src/views/cluster/containers/nodeGraphs/dashboards/sql.tsx index 9e4dfd48bab2..6b70462498a5 100644 --- a/pkg/ui/workspaces/db-console/src/views/cluster/containers/nodeGraphs/dashboards/sql.tsx +++ b/pkg/ui/workspaces/db-console/src/views/cluster/containers/nodeGraphs/dashboards/sql.tsx @@ -27,6 +27,7 @@ export default function (props: GraphDashboardProps) { return [ @@ -45,6 +46,7 @@ export default function (props: GraphDashboardProps) { @@ -59,6 +61,7 @@ export default function (props: GraphDashboardProps) { @@ -73,6 +76,7 @@ export default function (props: GraphDashboardProps) { @@ -84,6 +88,7 @@ export default function (props: GraphDashboardProps) { @@ -144,6 +151,7 @@ export default function (props: GraphDashboardProps) { @@ -162,6 +170,7 @@ export default function (props: GraphDashboardProps) { @@ -178,6 +187,7 @@ export default function (props: GraphDashboardProps) { @@ -195,6 +205,7 @@ export default function (props: GraphDashboardProps) { @@ -212,6 +223,7 @@ export default function (props: GraphDashboardProps) { Over the last minute, this node executed 99.99% of SQL statements @@ -238,6 +250,7 @@ export default function (props: GraphDashboardProps) { Over the last minute, this node executed 99.9% of SQL statements @@ -264,6 +277,7 @@ export default function (props: GraphDashboardProps) { Over the last minute, this node executed 99% of SQL statements within @@ -290,6 +304,7 @@ export default function (props: GraphDashboardProps) { Over the last minute, this node executed 90% of SQL statements within @@ -316,6 +331,7 @@ export default function (props: GraphDashboardProps) { @@ -334,6 +350,7 @@ export default function (props: GraphDashboardProps) { @@ -352,6 +369,7 @@ export default function (props: GraphDashboardProps) { @@ -433,6 +452,7 @@ export default function (props: GraphDashboardProps) { Over the last minute, this node executed 99% of transactions within @@ -459,6 +479,7 @@ export default function (props: GraphDashboardProps) { Over the last minute, this node executed 90% of transactions within @@ -485,6 +506,7 @@ export default function (props: GraphDashboardProps) { @@ -503,6 +525,7 @@ export default function (props: GraphDashboardProps) { @@ -517,6 +540,7 @@ export default function (props: GraphDashboardProps) { + , - + , @@ -75,6 +76,7 @@ export default function (props: GraphDashboardProps) { , diff --git a/pkg/ui/workspaces/db-console/src/views/cluster/containers/nodeGraphs/index.tsx b/pkg/ui/workspaces/db-console/src/views/cluster/containers/nodeGraphs/index.tsx index 81bd8711b389..40a7f140eb1f 100644 --- a/pkg/ui/workspaces/db-console/src/views/cluster/containers/nodeGraphs/index.tsx +++ b/pkg/ui/workspaces/db-console/src/views/cluster/containers/nodeGraphs/index.tsx @@ -87,28 +87,67 @@ import { selectResolution30mStorageTTL, selectCrossClusterReplicationEnabled, } from "src/redux/clusterSettings"; +import { getDataFromServer } from "src/util/dataFromServer"; interface GraphDashboard { label: string; component: (props: GraphDashboardProps) => React.ReactElement[]; + isKvDashboard: boolean; } const dashboards: { [key: string]: GraphDashboard } = { - overview: { label: "Overview", component: overviewDashboard }, - hardware: { label: "Hardware", component: hardwareDashboard }, - runtime: { label: "Runtime", component: runtimeDashboard }, - sql: { label: "SQL", component: sqlDashboard }, - storage: { label: "Storage", component: storageDashboard }, - replication: { label: "Replication", component: replicationDashboard }, - distributed: { label: "Distributed", component: distributedDashboard }, - queues: { label: "Queues", component: queuesDashboard }, - requests: { label: "Slow Requests", component: requestsDashboard }, - changefeeds: { label: "Changefeeds", component: changefeedsDashboard }, - overload: { label: "Overload", component: overloadDashboard }, - ttl: { label: "TTL", component: ttlDashboard }, + overview: { + label: "Overview", + component: overviewDashboard, + isKvDashboard: false, + }, + hardware: { + label: "Hardware", + component: hardwareDashboard, + isKvDashboard: true, + }, + runtime: { + label: "Runtime", + component: runtimeDashboard, + isKvDashboard: true, + }, + sql: { label: "SQL", component: sqlDashboard, isKvDashboard: false }, + storage: { + label: "Storage", + component: storageDashboard, + isKvDashboard: true, + }, + replication: { + label: "Replication", + component: replicationDashboard, + isKvDashboard: true, + }, + distributed: { + label: "Distributed", + component: distributedDashboard, + isKvDashboard: true, + }, + queues: { label: "Queues", component: queuesDashboard, isKvDashboard: true }, + requests: { + label: "Slow Requests", + component: requestsDashboard, + isKvDashboard: true, + }, + changefeeds: { + label: "Changefeeds", + component: changefeedsDashboard, + isKvDashboard: false, + }, + overload: { + label: "Overload", + component: overloadDashboard, + isKvDashboard: true, + }, + ttl: { label: "TTL", component: ttlDashboard, isKvDashboard: false }, crossClusterReplication: { label: "Cross-Cluster Replication", component: crossClusterReplicationDashboard, + isKvDashboard: true, }, }; @@ -118,6 +157,7 @@ const dashboardDropdownOptions = _.map(dashboards, (dashboard, key) => { return { value: key, label: dashboard.label, + isKvDashboard: dashboard.isKvDashboard, }; }); @@ -253,8 +293,13 @@ export class NodeGraphs extends React.Component< nodeDisplayNameByID, nodeIds, } = this.props; + const canViewKvGraphs = + getDataFromServer().FeatureFlags.can_view_kv_metric_dashboards; const { showLowResolutionAlert, showDeletedDataAlert } = this.state; - const selectedDashboard = getMatchParamByName(match, dashboardNameAttr); + let selectedDashboard = getMatchParamByName(match, dashboardNameAttr); + if (dashboards[selectedDashboard].isKvDashboard && !canViewKvGraphs) { + selectedDashboard = defaultDashboard; + } const dashboard = _.has(dashboards, selectedDashboard) ? selectedDashboard : defaultDashboard; @@ -300,7 +345,9 @@ export class NodeGraphs extends React.Component< // Generate graphs for the current dashboard, wrapping each one in a // MetricsDataProvider with a unique key. - const graphs = dashboards[dashboard].component(dashboardProps); + const graphs = dashboards[dashboard] + .component(dashboardProps) + .filter(d => canViewKvGraphs || !d.props.isKvGraph); const graphComponents = _.map(graphs, (graph, idx) => { const key = `nodes.${dashboard}.${idx}`; return ( @@ -325,12 +372,15 @@ export class NodeGraphs extends React.Component< // as we have 3 columns, we divide node amount on 3 const paddingBottom = nodeIDs.length > 8 ? 90 + Math.ceil(nodeIDs.length / 3) * 10 : 50; - - const filteredDropdownOptions = this.props.crossClusterReplicationEnabled - ? dashboardDropdownOptions // Already in the list, no need to filter - : dashboardDropdownOptions.filter( - option => option.label !== "Cross-Cluster Replication", - ); + const filteredDropdownOptions = dashboardDropdownOptions + // Don't show KV dashboards if the logged-in user doesn't have permission to view them. + .filter(option => canViewKvGraphs || !option.isKvDashboard) + // Don't show the replication dashboard if not enabled. + .filter( + option => + this.props.crossClusterReplicationEnabled || + option.label !== "Cross-Cluster Replication", + ); return (