diff --git a/src/layouts/admin-menu.jsx b/src/layouts/admin-menu.jsx
index a6faadeb..f66eee87 100644
--- a/src/layouts/admin-menu.jsx
+++ b/src/layouts/admin-menu.jsx
@@ -609,6 +609,14 @@ const renderMenu = (t) => {
children: [],
hasBreadcrumb: true,
},
+ {
+ path: '/monitor-center/storage-cluster-admin',
+ name: t('Storage Cluster'),
+ key: 'monitorStorageClusterAdmin',
+ level: 1,
+ children: [],
+ hasBreadcrumb: true,
+ },
],
},
{
diff --git a/src/pages/monitor/containers/StorageCluster/Charts.jsx b/src/pages/monitor/containers/StorageCluster/Charts.jsx
new file mode 100644
index 00000000..de5cf3c7
--- /dev/null
+++ b/src/pages/monitor/containers/StorageCluster/Charts.jsx
@@ -0,0 +1,377 @@
+// Copyright 2021 99cloud
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import React, { Component } from 'react';
+import { observer } from 'mobx-react';
+import { handleResponses } from 'components/PrometheusChart/utils/dataHandler';
+import ChartCard from 'components/PrometheusChart/ChartCard';
+import { ChartType } from 'components/PrometheusChart/utils/utils';
+import { Col, Progress, Row } from 'antd';
+import { merge, get } from 'lodash';
+import BaseCard from 'components/PrometheusChart/BaseCard';
+import {
+ cephStatusColorMap,
+ cephStatusMap,
+ getSuitableValue,
+} from 'resources/monitoring';
+import { computePercentage } from 'utils/index';
+import CircleChart from 'components/PrometheusChart/CircleWithRightLegend';
+import RenderTabs from './RenderTabs';
+import styles from './index.less';
+
+@observer
+class Charts extends Component {
+ constructor(props) {
+ super(props);
+ this.store = props.store;
+ }
+
+ renderTopCards() {
+ const baseConfig = {
+ span: 12,
+ constructorParams: {
+ requestType: 'current',
+ formatDataFn: handleResponses,
+ },
+ renderContent: (store) => {
+ const data = get(store, 'data[0].y', 0);
+ return
{data}
;
+ },
+ visibleHeight: 120,
+ };
+ const chartLists = [
+ {
+ title: t('Storage Cluster Status'),
+ span: 6,
+ constructorParams: {
+ metricKey: 'storageCluster.cephHealthStatus',
+ },
+ renderContent: (store) => {
+ const data = get(store.data, 'y', 0);
+ return (
+
+ {cephStatusMap[data]}
+
+ );
+ },
+ },
+ {
+ title: 'Monitors',
+ span: 9,
+ constructorParams: {
+ metricKey: 'storageCluster.cephMonitorStatus',
+ formatDataFn: (...rest) => {
+ const data = handleResponses(...rest);
+ const status = [
+ {
+ type: 'down',
+ value: 0,
+ },
+ {
+ type: 'up',
+ value: 0,
+ },
+ ];
+ data.forEach((i) => {
+ status[i.y].value++;
+ });
+ return status;
+ },
+ },
+ renderContent: (store) => (
+
+ ),
+ },
+ {
+ title: 'PGs',
+ span: 9,
+ constructorParams: {
+ metricKey: 'storageCluster.cephPGS',
+ formatDataFn: (...rest) => {
+ const data = handleResponses(...rest);
+ return [
+ {
+ type: 'clean',
+ value: get(data, '[0].y', 0),
+ },
+ {
+ type: 'others',
+ value: get(data, '[1].y', 0),
+ },
+ ];
+ },
+ },
+ renderContent: (store) => (
+
+ ),
+ },
+ {
+ title: 'OSDs',
+ span: 9,
+ constructorParams: {
+ metricKey: 'storageCluster.osdData',
+ formatDataFn: (resps) => {
+ const [inUp, inDown, outUp, outDown] = resps;
+ return {
+ inUp: getValue(inUp),
+ inDown: getValue(inDown),
+ outUp: getValue(outUp),
+ outDown: getValue(outDown),
+ };
+
+ function getValue(d) {
+ return get(d, 'data.result[0].value[1]', 0);
+ }
+ },
+ },
+ renderContent: (store) => {
+ const { data } = store;
+ return (
+
+
+
+ {t('Up')}
+
+
+ {t('Down')}
+
+
+ {t('In Cluster')}
+
+
+ {data.inUp}
+
+
+ {data.inDown}
+
+
+ {t('Out Cluster')}
+
+
+ {data.outUp}
+
+
+ {data.outDown}
+
+
+ );
+ },
+ },
+ {
+ title: t('Average PGs per OSD'),
+ span: 5,
+ constructorParams: {
+ metricKey: 'storageCluster.avgPerOSD',
+ },
+ },
+ // {
+ // title: t('Average OSD Apply Latency(ms)'),
+ // span: 5,
+ // constructorParams: {
+ // metricKey: 'storageCluster.avgOSDApplyLatency',
+ // },
+ // },
+ // {
+ // title: t('Average OSD Commit Latency(ms)'),
+ // span: 5,
+ // constructorParams: {
+ // metricKey: 'storageCluster.avgOSDCommitLatency',
+ // },
+ // },
+ {
+ title: t('Storage Cluster Usage'),
+ span: 10,
+ constructorParams: {
+ metricKey: 'storageCluster.storageClusterUsage',
+ },
+ renderContent: (store) => {
+ const { data } = store;
+ const usedValue = get(data[0], 'y', 0);
+ const totalValue = get(data[1], 'y', 0);
+ const used = getSuitableValue(usedValue, 'disk');
+ const total = getSuitableValue(totalValue, 'disk');
+ const progressPercentage = computePercentage(usedValue, totalValue);
+ return (
+
+
+
+
+ {`${t('Used')} ${used} / ${t('Total')} ${total}`}
+
+
+
+
+
+
+ );
+ },
+ },
+ ];
+ return (
+
+ {chartLists.map((chartProps) => {
+ const config = merge({}, baseConfig, chartProps);
+ const { span, ...rest } = config;
+ return (
+
+
+
+ );
+ })}
+
+ );
+ }
+
+ renderChartCards() {
+ const baseConfig = {
+ span: 12,
+ constructorParams: {
+ requestType: 'range',
+ formatDataFn: handleResponses,
+ },
+ chartProps: {
+ height: 250,
+ scale: {
+ y: {
+ nice: true,
+ },
+ },
+ },
+ };
+ const chartLists = [
+ {
+ title: t('Storage Pool Capacity Usage'),
+ constructorParams: {
+ metricKey: 'storageCluster.poolCapacityUsage',
+ modifyKeys: [t('used'), t('available')],
+ },
+ chartProps: {
+ chartType: ChartType.MULTILINE,
+ scale: {
+ y: {
+ formatter: (d) => getSuitableValue(d, 'disk', 0),
+ },
+ },
+ },
+ },
+ {
+ title: t('Storage Cluster OSD Latency'),
+ constructorParams: {
+ metricKey: 'storageCluster.clusterOSDLatency',
+ modifyKeys: ['apply', 'commit'],
+ },
+ chartProps: {
+ chartType: ChartType.MULTILINE,
+ },
+ },
+ {
+ title: t('Storage Cluster IOPS'),
+ constructorParams: {
+ metricKey: 'storageCluster.clusterIOPS',
+ modifyKeys: [t('read'), t('write')],
+ },
+ chartProps: {
+ chartType: ChartType.MULTILINE,
+ },
+ },
+ {
+ title: t('Storage Cluster Bandwidth'),
+ constructorParams: {
+ metricKey: 'storageCluster.clusterBandwidth',
+ modifyKeys: [t('in'), t('out')],
+ },
+ chartProps: {
+ scale: {
+ y: {
+ formatter: (d) => getSuitableValue(d, 'bandwidth', 0),
+ },
+ },
+ chartType: ChartType.MULTILINE,
+ },
+ },
+ ];
+ return (
+
+ {chartLists.map((chartProps) => {
+ const config = merge({}, baseConfig, chartProps);
+ const { span, ...rest } = config;
+ return (
+
+
+
+ );
+ })}
+
+ );
+ }
+
+ render() {
+ if (this.store.isLoading) {
+ return null;
+ }
+ return (
+
+ {this.renderTopCards()}
+ {this.renderChartCards()}
+
+
+
+
+ );
+ }
+}
+
+export default Charts;
diff --git a/src/pages/monitor/containers/StorageCluster/RenderTabs.jsx b/src/pages/monitor/containers/StorageCluster/RenderTabs.jsx
new file mode 100644
index 00000000..c90d9a35
--- /dev/null
+++ b/src/pages/monitor/containers/StorageCluster/RenderTabs.jsx
@@ -0,0 +1,285 @@
+// Copyright 2021 99cloud
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import React, { Component } from 'react';
+import { Spin, Tabs } from 'antd';
+import { observer } from 'mobx-react';
+import { formatSize } from 'utils/index';
+import FetchPrometheusStore from 'components/PrometheusChart/store/FetchPrometheusStore';
+import { get } from 'lodash';
+import metricDict from 'components/PrometheusChart/metricDict';
+import { fetchPrometheus } from 'components/PrometheusChart/utils/utils';
+import BaseTable from 'components/Tables/Base';
+import List from 'stores/base-list';
+import styles from './index.less';
+
+const { TabPane } = Tabs;
+
+@observer
+class RenderTabs extends Component {
+ constructor(props) {
+ super(props);
+ this.store = new FetchPrometheusStore({
+ requestType: 'current',
+ metricKey: 'storageCluster.tabs',
+ modifyKeys: ['pools', 'osds'],
+ formatDataFn: this.formatDataFn,
+ });
+ this.state = {
+ filters: {},
+ };
+ }
+
+ componentDidMount() {
+ this.getData();
+ }
+
+ async getData() {
+ await this.store
+ .fetchData({
+ currentRange: this.props.store.currentRange,
+ interval: this.props.store.interval,
+ })
+ .then(() => {
+ this.getListData();
+ });
+ }
+
+ async getListData() {
+ const { data } = this.store;
+ const newData = [...data];
+ const poolPromises = get(metricDict, 'storageCluster.poolTab.url', []).map(
+ (item) => fetchPrometheus(item, 'current')
+ );
+ const osdPromises = get(metricDict, 'storageCluster.osdTab.url', []).map(
+ (item) => fetchPrometheus(item, 'current')
+ );
+
+ const poolRets = await Promise.all(poolPromises);
+ poolRets.forEach((ret, index) => {
+ handler(ret, index, 'pool_id');
+ });
+
+ const osdRets = await Promise.all(osdPromises);
+ osdRets.forEach((ret, index) => {
+ handler(ret, index, 'ceph_daemon');
+ });
+ this.store.updateData(newData);
+
+ function handler(ret, index, primaryKey) {
+ ret.data.result.forEach((item) => {
+ const { metric, value } = item;
+ const itemIndex = newData.findIndex(
+ (p) => p[primaryKey] === metric[primaryKey]
+ );
+ // 特殊处理计算表达式没有metric.__name__的情况,
+ // 此处usage是details的第3个查询表达式,所以判断index===3, 两个Tab放一起处理了,都放在第3个。
+ // console.log(metric.__name__);
+ if (index === 3) {
+ newData[itemIndex].usage = parseFloat(
+ parseFloat(value[1]).toFixed(2)
+ );
+ } else if (
+ [
+ 'ceph_pool_objects',
+ 'ceph_pg_total',
+ 'ceph_pool_max_avail',
+ 'ceph_osd_weight',
+ 'ceph_osd_apply_latency_ms',
+ 'ceph_osd_commit_latency_ms',
+ 'ceph_osd_stat_bytes',
+ ].indexOf(metric.__name__) > -1
+ ) {
+ newData[itemIndex][metric.__name__] = parseInt(value[1], 10);
+ } else {
+ newData[itemIndex][metric.__name__] = value[1];
+ }
+ });
+ }
+ }
+
+ get tableData() {
+ const { filters } = this.state;
+ let originData = this.store.data.filter(
+ (d) => d.type === (this.store.device || this.store.data[0].type)
+ );
+ Object.keys(filters).forEach((key) => {
+ originData = originData.filter((i) => i[key] === filters[key]);
+ });
+ return originData;
+ }
+
+ formatDataFn(resps) {
+ const retData = [];
+ const [pool, osd] = resps;
+ get(pool, 'data.result', []).forEach((r) => {
+ const { metric, value } = r;
+ retData.push({
+ type: 'pool',
+ ...metric,
+ value: parseFloat(value[1]) || 0,
+ });
+ });
+ get(osd, 'data.result', []).forEach((r) => {
+ const { metric, value } = r;
+ retData.push({
+ type: 'osd',
+ ...metric,
+ value: parseFloat(value[1]) || 0,
+ });
+ });
+ return retData;
+ }
+
+ render() {
+ const { device = 'pool' } = this.store;
+ const columns = device === 'pool' ? poolsColumns : osdsColumns;
+ return (
+ <>
+ {
+ this.setState(
+ {
+ filters: {},
+ },
+ () => {
+ this.store.handleDeviceChange(e);
+ }
+ );
+ }}
+ >
+
+
+
+ {this.store.isLoading ? (
+
+
+
+ ) : (
+ {
+ const { limit, page, sortKey, sortOrder, ...rest } = filters;
+ this.setState({
+ filters: rest,
+ });
+ }}
+ onFetchBySort={() => {}}
+ />
+ )}
+ >
+ );
+ }
+}
+
+export default RenderTabs;
+
+const poolsColumns = [
+ {
+ title: t('Pool Name'),
+ dataIndex: 'name',
+ },
+ {
+ title: t('PGs'),
+ dataIndex: 'ceph_pg_total',
+ isHideable: true,
+ },
+ {
+ title: t('Objects'),
+ dataIndex: 'ceph_pool_objects',
+ isHideable: true,
+ },
+ {
+ title: t('Max Avail'),
+ dataIndex: 'ceph_pool_max_avail',
+ render: (text) => formatSize(text),
+ isHideable: true,
+ },
+ {
+ title: t('Usage'),
+ dataIndex: 'usage',
+ render: (text) => `${text}%`,
+ isHideable: true,
+ },
+];
+
+const osdsColumns = [
+ {
+ title: t('Name'),
+ dataIndex: 'ceph_daemon',
+ },
+ {
+ title: t('Status'),
+ dataIndex: 'ceph_osd_up',
+ render: (up) => (up === '1' ? t('Up') : t('Down')),
+ isHideable: true,
+ },
+ {
+ title: t('Instance Addr'),
+ dataIndex: 'cluster_addr',
+ isHideable: true,
+ },
+ {
+ title: t('Weight'),
+ dataIndex: 'ceph_osd_weight',
+ isHideable: true,
+ },
+ {
+ title: t('Apply Latency(ms)'),
+ dataIndex: 'ceph_osd_apply_latency_ms',
+ isHideable: true,
+ },
+ {
+ title: t('Commit Latency(ms)'),
+ dataIndex: 'ceph_osd_commit_latency_ms',
+ isHideable: true,
+ },
+ {
+ title: t('Total Capacity'),
+ dataIndex: 'ceph_osd_stat_bytes',
+ render: (text) => formatSize(text),
+ isHideable: true,
+ },
+ {
+ title: t('Usage'),
+ dataIndex: 'usage',
+ render: (text) => `${parseFloat(text).toFixed(2)}%`,
+ isHideable: true,
+ },
+];
diff --git a/src/pages/monitor/containers/StorageCluster/TabTable.jsx b/src/pages/monitor/containers/StorageCluster/TabTable.jsx
new file mode 100644
index 00000000..668a71d1
--- /dev/null
+++ b/src/pages/monitor/containers/StorageCluster/TabTable.jsx
@@ -0,0 +1,72 @@
+// Copyright 2021 99cloud
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { observer } from 'mobx-react';
+import Base from 'containers/List';
+
+@observer
+export default class TabTable extends Base {
+ componentDidMount() {}
+
+ getData() {}
+
+ getDataSource = () => {
+ return this.list.data;
+ };
+
+ get params() {
+ return {};
+ }
+
+ get list() {
+ return {
+ data: this.props.store.data.filter(
+ (d) => d.type === (this.props.store.device || 'pool')
+ ),
+ filters: [],
+ };
+ }
+
+ get location() {
+ return {
+ search: [],
+ };
+ }
+
+ get rowKey() {
+ const { tabKey } = this.store;
+ return tabKey === 'pools' ? 'pool_id' : 'ceph_daemon';
+ }
+
+ get name() {
+ return t('tab tables');
+ }
+
+ get actionConfigs() {
+ return {
+ rowActions: {},
+ primaryActions: [],
+ };
+ }
+
+ getColumns = () => {
+ const { columns } = this.props;
+ return columns || [];
+ };
+
+ get searchFilters() {
+ const { searchFilters } = this.props;
+ return searchFilters || [];
+ }
+}
diff --git a/src/pages/monitor/containers/StorageCluster/index.jsx b/src/pages/monitor/containers/StorageCluster/index.jsx
new file mode 100644
index 00000000..d216b106
--- /dev/null
+++ b/src/pages/monitor/containers/StorageCluster/index.jsx
@@ -0,0 +1,34 @@
+// Copyright 2021 99cloud
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import React from 'react';
+import { observer } from 'mobx-react';
+import BaseContent from 'components/PrometheusChart/component/BaseContent';
+import Charts from './Charts';
+
+const StorageCluster = () => {
+ const BaseContentConfig = {
+ renderNodeSelect: false,
+ };
+
+ return (
+
+ );
+
+ function renderChartCards(store) {
+ return ;
+ }
+};
+
+export default observer(StorageCluster);
diff --git a/src/pages/monitor/containers/StorageCluster/index.less b/src/pages/monitor/containers/StorageCluster/index.less
new file mode 100644
index 00000000..50ebbda1
--- /dev/null
+++ b/src/pages/monitor/containers/StorageCluster/index.less
@@ -0,0 +1,119 @@
+.OSDs {
+ height: 100%;
+ font-size: 16px;
+ text-align: center;
+ font-weight: 500;
+ color: rgba(0, 0, 0, 0.85);
+}
+.header {
+ overflow: auto;
+ padding: 20px;
+
+ .range {
+ :global {
+ .ant-radio-button-wrapper {
+ color: rgba(0, 0, 0, 0.65);
+ }
+
+ .ant-radio-button-wrapper-checked {
+ color: #0068ff;
+ }
+ }
+ }
+
+ .download {
+ float: right;
+
+ :global {
+ .ant-btn-icon-only {
+ border-radius: 4px;
+ }
+ }
+ }
+}
+
+.myCardRow {
+ .top {
+ .content {
+ font-size: 24px;
+ text-align: center;
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ :global {
+ .ant-card-bordered {
+ display: flex;
+ flex-direction: column;
+
+ .ant-card-body {
+ flex-grow: 1;
+ overflow: hidden;
+ padding-top: 0;
+ }
+ }
+ }
+ }
+
+ :global {
+ .ant-card-bordered {
+ box-shadow: 0px 2px 20px 0px rgba(0, 0, 0, 0.09);
+
+ .ant-card-head {
+ border-bottom: none;
+ }
+ }
+ }
+}
+
+.outer {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ overflow: hidden;
+ font-size: 12px;
+
+ .inner {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ left: 0;
+ overflow-x: hidden;
+ overflow-y: scroll;
+ }
+
+ .inner::-webkit-scrollbar {
+ display: none;
+ }
+}
+
+.topContent {
+ font-size: 24px;
+ font-weight: 500;
+ height: 120px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.tabs {
+ :global {
+ .ant-tabs-tab {
+ margin-right: 20px;
+ border-bottom: 1px solid #f0f0f0;
+ }
+
+ .ant-tabs-nav::before {
+ border-bottom: none;
+ }
+ }
+}
+
+.spinContainer {
+ width: 100%;
+ min-height: 400px;
+ padding: 30px 50px;
+ text-align: center;
+}
diff --git a/src/pages/monitor/routes/index.js b/src/pages/monitor/routes/index.js
index 10feacb5..d795dd42 100644
--- a/src/pages/monitor/routes/index.js
+++ b/src/pages/monitor/routes/index.js
@@ -15,6 +15,7 @@
import BaseLayout from 'layouts/Basic';
import E404 from 'pages/base/containers/404';
import PhysicalNode from '../containers/PhysicalNode';
+import StorageCluster from '../containers/StorageCluster';
import Overview from '../containers/Overview';
const PATH = '/monitor-center';
@@ -29,6 +30,11 @@ export default [
component: PhysicalNode,
exact: true,
},
+ {
+ path: `${PATH}/storage-cluster-admin`,
+ component: StorageCluster,
+ exact: true,
+ },
{ path: '*', component: E404 },
],
},