mirror of
https://github.com/apache/rocketmq-dashboard.git
synced 2025-09-10 03:29:59 +08:00
[GSOC][RIP-78][ISSUES#308] Add part of refactored front-end files (#310)
This commit is contained in:
108
frontend-new/src/assets/styles/theme.js
Normal file
108
frontend-new/src/assets/styles/theme.js
Normal file
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.
|
||||
*/
|
||||
export const defaultTheme = {
|
||||
token: {
|
||||
colorPrimary: '#0cb5fb', // 主题色
|
||||
borderRadius: 1.5, // 组件圆角
|
||||
},
|
||||
components: {
|
||||
Button: {
|
||||
colorPrimary: '#1c324a', // 普通按钮主题色
|
||||
},
|
||||
Layout: {
|
||||
headerBg: '#1c324a', // 设置 Header 的背景色为 #1c324a
|
||||
headerColor: '#ffffff', // 设置 Header 内文本颜色为白色
|
||||
backgroundColor: '#ffffff', // 设置 Layout 的背景色为白色
|
||||
colorBgLayout: '#f9fcfe',
|
||||
},
|
||||
Menu: {
|
||||
darkItemBg: '#1c324a',
|
||||
horizontalItemSelectedBg: '#0cb5fb',
|
||||
itemSelectedColor: '#ffffff',
|
||||
itemColor: '#ffffff',
|
||||
colorText: 'rgba(255, 255, 255, 0.88)', // Adjust for dark theme menu
|
||||
activeBarBorderWidth: 0,
|
||||
},
|
||||
Drawer: {
|
||||
colorBgElevated: '#1c324a', // Drawer 背景色
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const pinkTheme = {
|
||||
token: {
|
||||
colorPrimary: '#FF69B4', // 热粉色
|
||||
borderRadius: 1.5,
|
||||
},
|
||||
components: {
|
||||
Button: {
|
||||
colorPrimary: '#FFC0CB', // 深粉色
|
||||
},
|
||||
Layout: {
|
||||
headerBg: '#FFC0CB', // 粉色
|
||||
headerColor: '#000000', // 黑色文本
|
||||
backgroundColor: '#F8F8FF', // 幽灵白
|
||||
colorBgLayout: '#faf4f4',
|
||||
},
|
||||
Menu: {
|
||||
darkItemBg: '#FFC0CB', // 粉色
|
||||
horizontalItemSelectedBg: '#FF69B4',
|
||||
itemSelectedColor: '#ffffff',
|
||||
itemColor: '#000000', // 黑色文本
|
||||
colorText: 'rgba(0, 0, 0, 0.88)',
|
||||
activeBarBorderWidth: 0,
|
||||
},
|
||||
Drawer: {
|
||||
colorBgElevated: '#FFC0CB', // Drawer 背景色
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const greenTheme = {
|
||||
token: {
|
||||
colorPrimary: '#52c41a', // 绿色
|
||||
borderRadius: 1.5,
|
||||
},
|
||||
components: {
|
||||
Button: {
|
||||
colorPrimary: '#7cb305', // 橄榄绿
|
||||
},
|
||||
Layout: {
|
||||
headerBg: '#3f673f', // 深绿色
|
||||
headerColor: '#ffffff', // 白色文本
|
||||
backgroundColor: '#f6ffed',
|
||||
colorBgLayout: '#ebf8eb',
|
||||
},
|
||||
Menu: {
|
||||
darkItemBg: '#3f673f', // 深绿色
|
||||
horizontalItemSelectedBg: '#52c41a',
|
||||
itemSelectedColor: '#ffffff',
|
||||
itemColor: '#ffffff',
|
||||
colorText: 'rgba(255, 255, 255, 0.88)',
|
||||
activeBarBorderWidth: 0,
|
||||
},
|
||||
Drawer: {
|
||||
colorBgElevated: '#3f673f', // Drawer 背景色
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const themes = {
|
||||
default: defaultTheme,
|
||||
pink: pinkTheme,
|
||||
green: greenTheme,
|
||||
};
|
73
frontend-new/src/components/DlqMessageDetailViewDialog.jsx
Normal file
73
frontend-new/src/components/DlqMessageDetailViewDialog.jsx
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 { Form, Input, Typography, Modal } from 'antd';
|
||||
import moment from 'moment';
|
||||
import { useLanguage } from '../i18n/LanguageContext'; // 根据实际路径调整
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const DlqMessageDetailViewDialog = ({ ngDialogData }) => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const messageView = ngDialogData?.messageView || {};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<Form layout="horizontal" labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
|
||||
<Form.Item label="Message ID:">
|
||||
<Text strong>{messageView.msgId}</Text>
|
||||
</Form.Item>
|
||||
<Form.Item label="Topic:">
|
||||
<Text strong>{messageView.topic}</Text>
|
||||
</Form.Item>
|
||||
<Form.Item label="Properties:">
|
||||
<Input.TextArea
|
||||
value={typeof messageView.properties === 'object' ? JSON.stringify(messageView.properties, null, 2) : messageView.properties}
|
||||
style={{ minHeight: 100, resize: 'none' }}
|
||||
readOnly
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="ReconsumeTimes:">
|
||||
<Text strong>{messageView.reconsumeTimes}</Text>
|
||||
</Form.Item>
|
||||
<Form.Item label="Tag:">
|
||||
<Text strong>{messageView.properties?.TAGS}</Text>
|
||||
</Form.Item>
|
||||
<Form.Item label="Key:">
|
||||
<Text strong>{messageView.properties?.KEYS}</Text>
|
||||
</Form.Item>
|
||||
<Form.Item label="Storetime:">
|
||||
<Text strong>{moment(messageView.storeTimestamp).format('YYYY-MM-DD HH:mm:ss')}</Text>
|
||||
</Form.Item>
|
||||
<Form.Item label="StoreHost:">
|
||||
<Text strong>{messageView.storeHost}</Text>
|
||||
</Form.Item>
|
||||
<Form.Item label="Message body:">
|
||||
<Input.TextArea
|
||||
value={messageView.messageBody}
|
||||
style={{ minHeight: 100, resize: 'none' }}
|
||||
readOnly
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DlqMessageDetailViewDialog;
|
208
frontend-new/src/components/MessageDetailViewDialog.jsx
Normal file
208
frontend-new/src/components/MessageDetailViewDialog.jsx
Normal file
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 { Modal, Button, Typography, Descriptions, Tag, Spin, notification } from 'antd';
|
||||
import moment from 'moment';
|
||||
import { ExclamationCircleOutlined, SyncOutlined } from '@ant-design/icons';
|
||||
import { useLanguage } from '../i18n/LanguageContext';
|
||||
import { remoteApi } from '../api/remoteApi/remoteApi'; // 确保这个路径正确
|
||||
|
||||
const { Text, Paragraph } = Typography;
|
||||
|
||||
const MessageDetailViewDialog = ({ visible, onCancel, messageId, topic, onResendMessage }) => {
|
||||
const { t } = useLanguage();
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [messageDetail, setMessageDetail] = React.useState(null);
|
||||
const [error, setError] = React.useState(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const fetchMessageDetail = async () => {
|
||||
// 只有当 visible 为 true 且 messageId 和 topic 存在时才进行数据请求
|
||||
if (!visible || !messageId || !topic) {
|
||||
// 如果 Modal 不可见或者必要参数缺失,则不加载数据
|
||||
setMessageDetail(null); // 清空旧数据
|
||||
setError(null); // 清空错误信息
|
||||
setLoading(false); // 停止加载状态
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null); // 在每次新的请求前清除之前的错误
|
||||
try {
|
||||
const resp = await remoteApi.viewMessage(messageId, topic);
|
||||
if (resp.status === 0) {
|
||||
setMessageDetail(resp.data);
|
||||
} else {
|
||||
const errorMessage = resp.errMsg || t.FETCH_MESSAGE_DETAIL_FAILED;
|
||||
setError(errorMessage);
|
||||
notification.error({
|
||||
message: t.ERROR,
|
||||
description: errorMessage,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = t.FETCH_MESSAGE_DETAIL_FAILED;
|
||||
setError(errorMessage);
|
||||
notification.error({
|
||||
message: t.ERROR,
|
||||
description: errorMessage,
|
||||
});
|
||||
console.error("Error fetching message detail:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchMessageDetail();
|
||||
}, [visible, messageId, topic, t]); // 依赖项中添加 visible,确保在 Modal 显示时触发
|
||||
|
||||
|
||||
// handleShowExceptionDesc 方法不再需要,因为我们直接使用 Paragraph 的 ellipsis
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t.MESSAGE_DETAIL}
|
||||
open={visible} // Ant Design 5.x 版本中,visible 属性已更名为 open
|
||||
onCancel={onCancel}
|
||||
footer={[
|
||||
<Button key="close" onClick={onCancel}>
|
||||
{t.CLOSE}
|
||||
</Button>,
|
||||
]}
|
||||
width={900}
|
||||
destroyOnHidden={true} // 使用新的 destroyOnHidden 替代 destroyOnClose
|
||||
>
|
||||
<Spin spinning={loading} tip={t.LOADING}>
|
||||
{error && (
|
||||
<Paragraph type="danger" style={{ textAlign: 'center' }}>
|
||||
{error}
|
||||
</Paragraph>
|
||||
)}
|
||||
{messageDetail ? ( // 确保 messageDetail 存在时才渲染内容
|
||||
<>
|
||||
{/* 消息信息部分 */}
|
||||
<Descriptions title={<Text strong>{t.MESSAGE_INFO}</Text>} bordered column={2} size="small" style={{ marginBottom: 20 }}>
|
||||
<Descriptions.Item label="Topic" span={2}><Text copyable>{messageDetail.messageView.topic}</Text></Descriptions.Item>
|
||||
<Descriptions.Item label="Message ID" span={2}><Text copyable>{messageDetail.messageView.msgId}</Text></Descriptions.Item>
|
||||
<Descriptions.Item label="StoreHost">{messageDetail.messageView.storeHost}</Descriptions.Item>
|
||||
<Descriptions.Item label="BornHost">{messageDetail.messageView.bornHost}</Descriptions.Item>
|
||||
<Descriptions.Item label="StoreTime">
|
||||
{moment(messageDetail.messageView.storeTimestamp).format("YYYY-MM-DD HH:mm:ss")}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="BornTime">
|
||||
{moment(messageDetail.messageView.bornTimestamp).format("YYYY-MM-DD HH:mm:ss")}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Queue ID">{messageDetail.messageView.queueId}</Descriptions.Item>
|
||||
<Descriptions.Item label="Queue Offset">{messageDetail.messageView.queueOffset}</Descriptions.Item>
|
||||
<Descriptions.Item label="StoreSize">{messageDetail.messageView.storeSize} bytes</Descriptions.Item>
|
||||
<Descriptions.Item label="ReconsumeTimes">{messageDetail.messageView.reconsumeTimes}</Descriptions.Item>
|
||||
<Descriptions.Item label="BodyCRC">{messageDetail.messageView.bodyCRC}</Descriptions.Item>
|
||||
<Descriptions.Item label="SysFlag">{messageDetail.messageView.sysFlag}</Descriptions.Item>
|
||||
<Descriptions.Item label="Flag">{messageDetail.messageView.flag}</Descriptions.Item>
|
||||
<Descriptions.Item label="PreparedTransactionOffset">{messageDetail.messageView.preparedTransactionOffset}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
{/* 消息属性部分 */}
|
||||
{Object.keys(messageDetail.messageView.properties).length > 0 && (
|
||||
<Descriptions title={<Text strong>{t.MESSAGE_PROPERTIES}</Text>} bordered column={1} size="small" style={{ marginBottom: 20 }}>
|
||||
{Object.entries(messageDetail.messageView.properties).map(([key, value]) => (
|
||||
<Descriptions.Item label={key} key={key}><Text copyable>{value}</Text></Descriptions.Item>
|
||||
))}
|
||||
</Descriptions>
|
||||
)}
|
||||
|
||||
{/* 消息体部分 */}
|
||||
<Descriptions title={<Text strong>{t.MESSAGE_BODY}</Text>} bordered column={1} size="small" style={{ marginBottom: 20 }}>
|
||||
<Descriptions.Item>
|
||||
<Paragraph
|
||||
copyable
|
||||
ellipsis={{
|
||||
rows: 5,
|
||||
expandable: true,
|
||||
symbol: t.SHOW_ALL_CONTENT,
|
||||
}}
|
||||
>
|
||||
{messageDetail.messageView.messageBody}
|
||||
</Paragraph>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
{/* 消息轨迹列表部分 */}
|
||||
{messageDetail.messageTrackList && messageDetail.messageTrackList.length > 0 ? (
|
||||
<>
|
||||
<Text strong>{t.MESSAGE_TRACKING}</Text>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
{messageDetail.messageTrackList.map((track, index) => (
|
||||
<Descriptions bordered column={1} size="small" key={index} style={{ marginBottom: 15 }}>
|
||||
<Descriptions.Item label={t.CONSUMER_GROUP}>
|
||||
{track.consumerGroup}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t.TRACK_TYPE}>
|
||||
<Tag color={
|
||||
track.trackType === 'CONSUMED_SOME_TIME_OK' ? 'success' :
|
||||
track.trackType === 'NOT_ONLINE' ? 'default' :
|
||||
track.trackType === 'PULL_SUCCESS' ? 'processing' :
|
||||
track.trackType === 'NO_MATCHED_CONSUMER' ? 'warning' :
|
||||
'error'
|
||||
}>
|
||||
{track.trackType}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t.OPERATION}>
|
||||
<Button
|
||||
icon={<SyncOutlined />}
|
||||
onClick={() => onResendMessage(messageDetail.messageView, track.consumerGroup)}
|
||||
size="small"
|
||||
style={{ marginRight: 8 }}
|
||||
>
|
||||
{t.RESEND_MESSAGE}
|
||||
</Button>
|
||||
{/* 移除“查看异常”按钮,因为现在直接在下方展示可展开内容 */}
|
||||
</Descriptions.Item>
|
||||
{track.exceptionDesc && (
|
||||
<Descriptions.Item label={t.EXCEPTION_SUMMARY}>
|
||||
{/* 异常信息截断显示,点击“查看更多”可展开 */}
|
||||
<Paragraph
|
||||
ellipsis={{
|
||||
rows: 2, // 默认显示2行
|
||||
expandable: true,
|
||||
symbol: <Text style={{ color: '#1890ff', cursor: 'pointer' }}>{t.READ_MORE}</Text>, // 蓝色展开文本
|
||||
}}
|
||||
>
|
||||
{track.exceptionDesc}
|
||||
</Paragraph>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
</Descriptions>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Paragraph>{t.NO_TRACKING_INFO}</Paragraph>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
// 当 messageDetail 为 null 时,可以显示一个占位符或者不显示内容
|
||||
!loading && !error && <Paragraph style={{ textAlign: 'center' }}>{t.NO_MESSAGE_DETAIL_AVAILABLE}</Paragraph>
|
||||
)}
|
||||
</Spin>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageDetailViewDialog;
|
470
frontend-new/src/components/MessageTraceDetailViewDialog.jsx
Normal file
470
frontend-new/src/components/MessageTraceDetailViewDialog.jsx
Normal file
@@ -0,0 +1,470 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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, { useEffect, useRef } from 'react';
|
||||
import { Form, Input, Typography, Collapse, Table, Tag } from 'antd';
|
||||
import moment from 'moment';
|
||||
import { useLanguage } from '../i18n/LanguageContext';
|
||||
import Paragraph from "antd/es/skeleton/Paragraph";
|
||||
import * as echarts from 'echarts'; // Import ECharts
|
||||
|
||||
const { Text } = Typography;
|
||||
const { Panel } = Collapse;
|
||||
|
||||
// Constants for styling and formatting, derived from the example
|
||||
const SUCCESS_COLOR = '#75d874';
|
||||
const ERROR_COLOR = 'red';
|
||||
const UNKNOWN_COLOR = 'yellow';
|
||||
const TRANSACTION_COMMIT_COLOR = SUCCESS_COLOR;
|
||||
const TRANSACTION_ROLLBACK_COLOR = ERROR_COLOR;
|
||||
const TRANSACTION_UNKNOWN_COLOR = 'grey';
|
||||
const TIME_FORMAT_PATTERN = "YYYY-MM-DD HH:mm:ss.SSS";
|
||||
const DEFAULT_DISPLAY_DURATION = 10 * 1000;
|
||||
const TRANSACTION_CHECK_COST_TIME = 50; // transactionTraceNode do not have costTime, assume it cost 50ms
|
||||
|
||||
const MessageTraceDetailViewDialog = ({ ngDialogData }) => {
|
||||
const { t } = useLanguage();
|
||||
const messageTraceGraphRef = useRef(null);
|
||||
|
||||
const producerNode = ngDialogData?.producerNode;
|
||||
const subscriptionNodeList = ngDialogData?.subscriptionNodeList || [];
|
||||
const messageTraceViews = ngDialogData?.messageTraceViews || []; // This data structure seems redundant for the Gantt chart but can be used for extra tooltip details.
|
||||
|
||||
useEffect(() => {
|
||||
if (messageTraceGraphRef.current && ngDialogData) {
|
||||
const chart = echarts.init(messageTraceGraphRef.current);
|
||||
|
||||
let data = [];
|
||||
let dataZoomEnd = 100;
|
||||
let startTime = Number.MAX_VALUE;
|
||||
let endTime = 0;
|
||||
let messageGroups = []; // This will be our Y-axis categories
|
||||
|
||||
if (producerNode) {
|
||||
startTime = +producerNode.traceNode.beginTimestamp;
|
||||
endTime = +producerNode.traceNode.endTimestamp;
|
||||
}
|
||||
|
||||
// Helper functions from the provided example
|
||||
function buildNodeColor(traceNode) {
|
||||
if (traceNode.transactionState != null) {
|
||||
switch (traceNode.transactionState) {
|
||||
case 'COMMIT_MESSAGE':
|
||||
return TRANSACTION_COMMIT_COLOR;
|
||||
case 'ROLLBACK_MESSAGE':
|
||||
return TRANSACTION_ROLLBACK_COLOR;
|
||||
case 'UNKNOW':
|
||||
return TRANSACTION_UNKNOWN_COLOR;
|
||||
default:
|
||||
return ERROR_COLOR;
|
||||
}
|
||||
}
|
||||
switch (traceNode.status) {
|
||||
case 'FAILED': // Changed 'failed' to 'FAILED' to match backend typically
|
||||
return ERROR_COLOR;
|
||||
case 'UNKNOWN': // Changed 'unknown' to 'UNKNOWN'
|
||||
return UNKNOWN_COLOR;
|
||||
default:
|
||||
return SUCCESS_COLOR;
|
||||
}
|
||||
}
|
||||
|
||||
function formatXAxisTime(value) {
|
||||
let duration = Math.max(0, value - startTime);
|
||||
if (duration < 1000)
|
||||
return timeFormat(duration, 'ms');
|
||||
duration /= 1000;
|
||||
if (duration < 60)
|
||||
return timeFormat(duration, 's');
|
||||
duration /= 60;
|
||||
if (duration < 60)
|
||||
return timeFormat(duration, 'min');
|
||||
duration /= 60;
|
||||
return timeFormat(duration, 'h');
|
||||
}
|
||||
|
||||
function timeFormat(duration, unit) {
|
||||
return duration.toFixed(2) + unit;
|
||||
}
|
||||
|
||||
function buildTraceInfo(itemName, itemValue) {
|
||||
if (itemValue) {
|
||||
return `${itemName}: ${itemValue}<br />`
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function formatCostTimeStr(costTime) {
|
||||
if (costTime < 0) {
|
||||
return "";
|
||||
}
|
||||
let costTimeStr = costTime;
|
||||
if (costTime === 0) {
|
||||
costTimeStr = '<1'
|
||||
}
|
||||
return `${costTimeStr}ms`;
|
||||
}
|
||||
|
||||
function buildCostTimeInfo(costTime) {
|
||||
if (costTime < 0) {
|
||||
return "";
|
||||
}
|
||||
return `Cost Time: ${formatCostTimeStr(costTime)}<br/>`
|
||||
}
|
||||
function buildTimeStamp(timestamp) {
|
||||
if (timestamp < 0) {
|
||||
return 'N/A';
|
||||
}
|
||||
return moment(timestamp).format(TIME_FORMAT_PATTERN);
|
||||
}
|
||||
|
||||
function formatNodeToolTip(params) {
|
||||
let traceNode = params.data.traceData.traceNode;
|
||||
return `
|
||||
${buildCostTimeInfo(traceNode.costTime)}
|
||||
Status: ${traceNode.status}<br />
|
||||
${buildTraceInfo('Begin Timestamp', buildTimeStamp(traceNode.beginTimestamp))}
|
||||
${buildTraceInfo('End Timestamp', buildTimeStamp(traceNode.endTimestamp))}
|
||||
Client Host: ${traceNode.clientHost}<br />
|
||||
Store Host: ${traceNode.storeHost}<br />
|
||||
Retry Times: ${traceNode.retryTimes < 0 ? 'N/A' : traceNode.retryTimes}<br />
|
||||
${buildTraceInfo('Message Type', traceNode.msgType)}
|
||||
${buildTraceInfo('Transaction ID', traceNode.transactionId)}
|
||||
${buildTraceInfo('Transaction State', traceNode.transactionState)}
|
||||
${buildTraceInfo('From Transaction Check', traceNode.fromTransactionCheck)}
|
||||
`;
|
||||
}
|
||||
|
||||
function calcGraphTimestamp(timestamp, relativeTimeStamp, duration, addDuration) {
|
||||
if (timestamp > 0) {
|
||||
return timestamp;
|
||||
}
|
||||
if (duration < 0) {
|
||||
return relativeTimeStamp;
|
||||
}
|
||||
return addDuration ? relativeTimeStamp + duration : relativeTimeStamp - duration;
|
||||
}
|
||||
|
||||
function addTraceData(traceNode, index, groupName) {
|
||||
if (traceNode.beginTimestamp < 0 && traceNode.endTimestamp < 0) {
|
||||
return;
|
||||
}
|
||||
let beginTimestamp = calcGraphTimestamp(traceNode.beginTimestamp, traceNode.endTimestamp, traceNode.costTime, false);
|
||||
let endTimestamp = calcGraphTimestamp(traceNode.endTimestamp, traceNode.beginTimestamp, traceNode.costTime, true);
|
||||
if (endTimestamp === beginTimestamp) {
|
||||
endTimestamp = beginTimestamp + 1; // Ensure a minimum duration for visualization
|
||||
}
|
||||
data.push({
|
||||
name: groupName, // To display group name in tooltip or for internal reference
|
||||
value: [
|
||||
index,
|
||||
beginTimestamp,
|
||||
endTimestamp,
|
||||
traceNode.costTime
|
||||
],
|
||||
itemStyle: {
|
||||
normal: {
|
||||
color: buildNodeColor(traceNode),
|
||||
opacity: 1
|
||||
}
|
||||
},
|
||||
traceData: {
|
||||
traceNode: traceNode
|
||||
}
|
||||
});
|
||||
startTime = Math.min(startTime, beginTimestamp);
|
||||
endTime = Math.max(endTime, endTimestamp);
|
||||
}
|
||||
|
||||
// Populate data for the Gantt chart
|
||||
subscriptionNodeList.forEach(item => {
|
||||
messageGroups.push(item.subscriptionGroup);
|
||||
});
|
||||
|
||||
subscriptionNodeList.forEach((subscriptionNode, index) => {
|
||||
subscriptionNode.consumeNodeList.forEach(traceNode => addTraceData(traceNode, index, subscriptionNode.subscriptionGroup));
|
||||
});
|
||||
|
||||
if (producerNode) {
|
||||
messageGroups.push(producerNode.groupName);
|
||||
let producerNodeIndex = messageGroups.length - 1;
|
||||
addTraceData(producerNode.traceNode, producerNodeIndex, producerNode.groupName);
|
||||
producerNode.transactionNodeList.forEach(transactionNode => {
|
||||
transactionNode.beginTimestamp = Math.max(producerNode.traceNode.endTimestamp,
|
||||
transactionNode.endTimestamp - TRANSACTION_CHECK_COST_TIME);
|
||||
addTraceData(transactionNode, producerNodeIndex, producerNode.groupName);
|
||||
endTime = Math.max(endTime, transactionNode.endTimestamp);
|
||||
});
|
||||
}
|
||||
|
||||
let totalDuration = endTime - startTime;
|
||||
if (totalDuration > DEFAULT_DISPLAY_DURATION) {
|
||||
dataZoomEnd = (DEFAULT_DISPLAY_DURATION / totalDuration) * 100;
|
||||
}
|
||||
|
||||
function renderItem(params, api) {
|
||||
let messageGroupIndex = api.value(0); // Y-axis index
|
||||
let start = api.coord([api.value(1), messageGroupIndex]); // X-axis start time, Y-axis group index
|
||||
let end = api.coord([api.value(2), messageGroupIndex]); // X-axis end time, Y-axis group index
|
||||
let height = api.size([0, 1])[1] * 0.6; // Height of the bar
|
||||
|
||||
let rectShape = echarts.graphic.clipRectByRect({
|
||||
x: start[0],
|
||||
y: start[1] - height / 2,
|
||||
width: Math.max(end[0] - start[0], 1), // Ensure minimum width
|
||||
height: height
|
||||
}, {
|
||||
x: params.coordSys.x,
|
||||
y: params.coordSys.y,
|
||||
width: params.coordSys.width,
|
||||
height: params.coordSys.height
|
||||
});
|
||||
|
||||
return rectShape && {
|
||||
type: 'rect',
|
||||
transition: ['shape'],
|
||||
shape: rectShape,
|
||||
style: api.style({
|
||||
text: formatCostTimeStr(api.value(3)), // Display cost time on the bar
|
||||
textFill: '#000',
|
||||
textAlign: 'right'
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
formatter: function (params) {
|
||||
return formatNodeToolTip(params);
|
||||
}
|
||||
},
|
||||
title: {
|
||||
text: producerNode ? `Message Trace: ${producerNode.topic}` : "Message Trace",
|
||||
left: 'center'
|
||||
},
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'slider',
|
||||
filterMode: 'weakFilter',
|
||||
showDataShadow: false,
|
||||
top: 'bottom', // Position at the bottom
|
||||
start: 0,
|
||||
end: dataZoomEnd,
|
||||
labelFormatter: function (value) {
|
||||
return formatXAxisTime(value + startTime); // Adjust label to show relative time from start
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'inside',
|
||||
filterMode: 'weakFilter'
|
||||
}
|
||||
],
|
||||
grid: {
|
||||
height: 300,
|
||||
left: '10%', // Adjust left margin for Y-axis labels
|
||||
right: '10%'
|
||||
},
|
||||
xAxis: {
|
||||
min: startTime,
|
||||
scale: true,
|
||||
axisLabel: {
|
||||
formatter: function (value) {
|
||||
return formatXAxisTime(value);
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
data: messageGroups, // Use group names as Y-axis categories
|
||||
axisLabel: {
|
||||
formatter: function (value, index) {
|
||||
// Display the group name on the Y-axis
|
||||
return value;
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'custom',
|
||||
renderItem: renderItem,
|
||||
encode: {
|
||||
x: [1, 2], // Use beginTimestamp and endTimestamp for X-axis
|
||||
y: 0 // Use the index for Y-axis category
|
||||
},
|
||||
data: data
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
chart.setOption(option);
|
||||
|
||||
const resizeChart = () => chart.resize();
|
||||
window.addEventListener('resize', resizeChart);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', resizeChart);
|
||||
chart.dispose();
|
||||
};
|
||||
}
|
||||
}, [ngDialogData, t]); // Add t as a dependency for the useEffect hook
|
||||
|
||||
// ... (rest of your existing component code)
|
||||
const transactionColumns = [
|
||||
{ title: t.TIMESTAMP, dataIndex: 'beginTimestamp', key: 'beginTimestamp', align: 'center', render: (text) => moment(text).format('YYYY-MM-DD HH:mm:ss.SSS') },
|
||||
{ title: t.TRANSACTION_STATE, dataIndex: 'transactionState', key: 'transactionState', align: 'center', render: (text) => <Tag color={text === 'COMMIT_MESSAGE' ? 'green' : (text === 'ROLLBACK_MESSAGE' ? 'red' : 'default')}>{text}</Tag> },
|
||||
{ title: t.FROM_TRANSACTION_CHECK, dataIndex: 'fromTransactionCheck', key: 'fromTransactionCheck', align: 'center', render: (text) => (text ? <Tag color="blue">{t.YES}</Tag> : <Tag color="purple">{t.NO}</Tag>) },
|
||||
{ title: t.CLIENT_HOST, dataIndex: 'clientHost', key: 'clientHost', align: 'center' },
|
||||
{ title: t.STORE_HOST, dataIndex: 'storeHost', key: 'storeHost', align: 'center' },
|
||||
];
|
||||
|
||||
const consumeColumns = [
|
||||
{ title: t.BEGIN_TIMESTAMP, dataIndex: 'beginTimestamp', key: 'beginTimestamp', align: 'center', render: (text) => text < 0 ? 'N/A' : moment(text).format('YYYY-MM-DD HH:mm:ss.SSS') },
|
||||
{ title: t.END_TIMESTAMP, dataIndex: 'endTimestamp', key: 'endTimestamp', align: 'center', render: (text) => text < 0 ? 'N/A' : moment(text).format('YYYY-MM-DD HH:mm:ss.SSS') },
|
||||
{ title: t.COST_TIME, dataIndex: 'costTime', key: 'costTime', align: 'center', render: (text) => text < 0 ? 'N/A' : `${text === 0 ? '<1' : text}ms` },
|
||||
{ title: t.STATUS, dataIndex: 'status', key: 'status', align: 'center', render: (text) => <Tag color={text === 'SUCCESS' ? 'green' : (text === 'FAILED' ? 'red' : 'default')}>{text}</Tag> },
|
||||
{ title: t.RETRY_TIMES, dataIndex: 'retryTimes', key: 'retryTimes', align: 'center', render: (text) => text < 0 ? 'N/A' : text },
|
||||
{ title: t.CLIENT_HOST, dataIndex: 'clientHost', key: 'clientHost', align: 'center' },
|
||||
{ title: t.STORE_HOST, dataIndex: 'storeHost', key: 'storeHost', align: 'center' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px', backgroundColor: '#f0f2f5' }}>
|
||||
<div style={{ marginBottom: '20px', borderRadius: '8px', overflow: 'hidden', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)' }}>
|
||||
<Collapse defaultActiveKey={['messageTraceGraph']} expandIconPosition="right">
|
||||
<Panel header={<Typography.Title level={3} style={{ margin: 0, color: '#333' }}>{t.MESSAGE_TRACE_GRAPH}</Typography.Title>} key="messageTraceGraph">
|
||||
<div ref={messageTraceGraphRef} style={{ height: 500, width: '100%', backgroundColor: '#fff', padding: '10px' }}>
|
||||
{/* ECharts message trace graph will be rendered here */}
|
||||
{(!producerNode && subscriptionNodeList.length === 0) && (
|
||||
<Text type="secondary" style={{ display: 'block', textAlign: 'center', marginTop: '150px' }}>{t.TRACE_GRAPH_PLACEHOLDER}</Text>
|
||||
)}
|
||||
</div>
|
||||
</Panel>
|
||||
</Collapse>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '20px', borderRadius: '8px', overflow: 'hidden', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)' }}>
|
||||
<Collapse defaultActiveKey={['sendMessageTrace']} expandIconPosition="right">
|
||||
<Panel header={<Typography.Title level={3} style={{ margin: 0, color: '#333' }}>{t.SEND_MESSAGE_TRACE}</Typography.Title>} key="sendMessageTrace">
|
||||
{!producerNode ? (
|
||||
<Paragraph style={{ padding: '16px', textAlign: 'center', color: '#666' }}>{t.NO_PRODUCER_TRACE_DATA}</Paragraph>
|
||||
) : (
|
||||
<div style={{ padding: '16px', backgroundColor: '#fff' }}>
|
||||
<Typography.Title level={4} style={{ marginBottom: '20px' }}>
|
||||
{t.SEND_MESSAGE_INFO} : ( {t.MESSAGE_ID} <Text strong copyable>{producerNode.msgId}</Text> )
|
||||
</Typography.Title>
|
||||
<Form layout="vertical" colon={false}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: '16px' }}>
|
||||
<Form.Item label={<Text strong>{t.TOPIC}</Text>}>
|
||||
<Input value={producerNode.topic} readOnly />
|
||||
</Form.Item>
|
||||
<Form.Item label={<Text strong>{t.PRODUCER_GROUP}</Text>}>
|
||||
<Input value={producerNode.groupName} readOnly />
|
||||
</Form.Item>
|
||||
<Form.Item label={<Text strong>{t.MESSAGE_KEY}</Text>}>
|
||||
<Input value={producerNode.keys} readOnly />
|
||||
</Form.Item>
|
||||
<Form.Item label={<Text strong>{t.TAG}</Text>}>
|
||||
<Input value={producerNode.tags} readOnly />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={<Text strong>{t.BEGIN_TIMESTAMP}</Text>}>
|
||||
<Input value={moment(producerNode.traceNode.beginTimestamp).format('YYYY-MM-DD HH:mm:ss.SSS')} readOnly />
|
||||
</Form.Item>
|
||||
<Form.Item label={<Text strong>{t.END_TIMESTAMP}</Text>}>
|
||||
<Input value={moment(producerNode.traceNode.endTimestamp).format('YYYY-MM-DD HH:mm:ss.SSS')} readOnly />
|
||||
</Form.Item>
|
||||
<Form.Item label={<Text strong>{t.COST_TIME}</Text>}>
|
||||
<Input value={`${producerNode.traceNode.costTime === 0 ? '<1' : producerNode.traceNode.costTime}ms`} readOnly />
|
||||
</Form.Item>
|
||||
<Form.Item label={<Text strong>{t.MSG_TYPE}</Text>}>
|
||||
<Input value={producerNode.traceNode.msgType} readOnly />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={<Text strong>{t.CLIENT_HOST}</Text>}>
|
||||
<Input value={producerNode.traceNode.clientHost} readOnly />
|
||||
</Form.Item>
|
||||
<Form.Item label={<Text strong>{t.STORE_HOST}</Text>}>
|
||||
<Input value={producerNode.traceNode.storeHost} readOnly />
|
||||
</Form.Item>
|
||||
<Form.Item label={<Text strong>{t.RETRY_TIMES}</Text>}>
|
||||
<Input value={producerNode.traceNode.retryTimes} readOnly />
|
||||
</Form.Item>
|
||||
<Form.Item label={<Text strong>{t.OFFSET_MSG_ID}</Text>}>
|
||||
<Input value={producerNode.offSetMsgId} readOnly />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
{producerNode.transactionNodeList && producerNode.transactionNodeList.length > 0 && (
|
||||
<div style={{ marginTop: '30px' }}>
|
||||
<Typography.Title level={4} style={{ marginBottom: '15px' }}>{t.CHECK_TRANSACTION_INFO}:</Typography.Title>
|
||||
<Table
|
||||
columns={transactionColumns}
|
||||
dataSource={producerNode.transactionNodeList}
|
||||
rowKey={(record, index) => `transaction_${index}`}
|
||||
bordered
|
||||
pagination={false}
|
||||
size="middle"
|
||||
scroll={{ x: 'max-content' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Panel>
|
||||
</Collapse>
|
||||
</div>
|
||||
|
||||
<div style={{ borderRadius: '8px', overflow: 'hidden', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)' }}>
|
||||
<Collapse defaultActiveKey={['consumeMessageTrace']} expandIconPosition="right">
|
||||
<Panel header={<Typography.Title level={3} style={{ margin: 0, color: '#333' }}>{t.CONSUME_MESSAGE_TRACE}</Typography.Title>} key="consumeMessageTrace">
|
||||
{subscriptionNodeList.length === 0 ? (
|
||||
<Paragraph style={{ padding: '16px', textAlign: 'center', color: '#666' }}>{t.NO_CONSUMER_TRACE_DATA}</Paragraph>
|
||||
) : (
|
||||
<div style={{ padding: '16px', backgroundColor: '#fff' }}>
|
||||
{subscriptionNodeList.map(subscriptionNode => (
|
||||
<Collapse
|
||||
key={subscriptionNode.subscriptionGroup}
|
||||
style={{ marginBottom: '10px', border: '1px solid #e0e0e0', borderRadius: '4px' }}
|
||||
defaultActiveKey={[subscriptionNode.subscriptionGroup]}
|
||||
ghost
|
||||
>
|
||||
<Panel
|
||||
header={<Typography.Title level={4} style={{ margin: 0 }}>{t.SUBSCRIPTION_GROUP}: <Text strong>{subscriptionNode.subscriptionGroup}</Text></Typography.Title>}
|
||||
key={subscriptionNode.subscriptionGroup}
|
||||
>
|
||||
<Table
|
||||
columns={consumeColumns}
|
||||
dataSource={subscriptionNode.consumeNodeList}
|
||||
rowKey={(record, index) => `${subscriptionNode.subscriptionGroup}_${index}`}
|
||||
bordered
|
||||
pagination={false}
|
||||
size="middle"
|
||||
scroll={{ x: 'max-content' }}
|
||||
/>
|
||||
</Panel>
|
||||
</Collapse>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Panel>
|
||||
</Collapse>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageTraceDetailViewDialog;
|
23
frontend-new/src/store/actions/themeActions.js
Normal file
23
frontend-new/src/store/actions/themeActions.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.
|
||||
*/
|
||||
|
||||
export const SET_THEME = 'SET_THEME';
|
||||
|
||||
export const setTheme = (themeName) => ({
|
||||
type: SET_THEME,
|
||||
payload: themeName,
|
||||
});
|
39
frontend-new/src/store/context/ThemeContext.js
Normal file
39
frontend-new/src/store/context/ThemeContext.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 { useEffect } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { themes, defaultTheme } from '../../assets/styles/theme';
|
||||
import { setTheme } from '../actions/themeActions';
|
||||
|
||||
export const useTheme = () => {
|
||||
// 从 Redux store 中取出 currentThemeName
|
||||
const currentThemeName = useSelector(state => state.theme.currentThemeName);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
||||
const currentTheme = themes[currentThemeName] || defaultTheme;
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('appTheme', currentThemeName);
|
||||
}, [currentThemeName]);
|
||||
|
||||
return {
|
||||
currentTheme,
|
||||
currentThemeName,
|
||||
setCurrentThemeName: (themeName) => dispatch(setTheme(themeName)),
|
||||
};
|
||||
};
|
28
frontend-new/src/store/index.js
Normal file
28
frontend-new/src/store/index.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 { createStore,combineReducers } from 'redux';
|
||||
import themeReducer from './reducers/themeReducer';
|
||||
|
||||
// 组合所有的 reducers
|
||||
const rootReducer = combineReducers({
|
||||
theme: themeReducer, // theme 状态将通过 state.theme 访问
|
||||
});
|
||||
|
||||
// 创建 Redux store
|
||||
const store = createStore(rootReducer);
|
||||
|
||||
export default store;
|
41
frontend-new/src/store/reducers/themeReducer.js
Normal file
41
frontend-new/src/store/reducers/themeReducer.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 { SET_THEME } from '../actions/themeActions';
|
||||
|
||||
const getInitialTheme = () => {
|
||||
return localStorage.getItem('appTheme') || 'default';
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
currentThemeName: getInitialTheme(),
|
||||
};
|
||||
|
||||
const themeReducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case SET_THEME:
|
||||
// 注意:reducer 应该返回新的状态对象,而不是直接修改旧状态
|
||||
return {
|
||||
...state,
|
||||
currentThemeName: action.payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default themeReducer;
|
Reference in New Issue
Block a user