chore: add Copy request to alerting chart (#121)

* chore: add `Copy request` to alerting chart

* chore: update release notes

---------

Co-authored-by: yaojiping <yaojiping@infini.ltd>
This commit is contained in:
yaojp123 2025-02-13 12:18:36 +08:00 committed by GitHub
parent 9d120276d1
commit 4e8215482c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 122 additions and 39 deletions

View File

@ -18,7 +18,9 @@ Information about release notes of INFINI Console is provided here.
### Improvements ### Improvements
- add Buckets Diff to alerting rule - add Copy request to alerting chart
- add credential settings for agent in enrolling agent
- add collection mode to cluster editing
## 1.28.1 (2025-01-24) ## 1.28.1 (2025-01-24)

View File

@ -17,7 +17,7 @@ title: "版本历史"
### Improvements ### Improvements
- 告警规则新增分桶对比 - 告警图表新增复制请求
- 在注册 Agent 中新增 Agent 凭据设置 - 在注册 Agent 中新增 Agent 凭据设置
- 在集群编辑中新增采集模式 - 在集群编辑中新增采集模式

View File

@ -54,7 +54,8 @@ export default (props) => {
isEdit, isEdit,
fetchParamsCache, fetchParamsCache,
handleContextMenu, handleContextMenu,
isFullScreen isFullScreen,
onResultChange
} = props; } = props;
const { series = [] } = record; const { series = [] } = record;
@ -99,6 +100,7 @@ export default (props) => {
if (!refresh) { if (!refresh) {
setLoading(true) setLoading(true)
setData() setData()
onResultChange && onResultChange()
} }
if (isTimeSeries && !range) { if (isTimeSeries && !range) {
@ -189,8 +191,10 @@ export default (props) => {
} }
res.hits.hits = res.hits.hits || []; res.hits.hits = res.hits.hits || [];
setData(res.hits) setData(res.hits)
onResultChange && onResultChange(res)
} }
} else { } else {
const index_pattern = indices.join(',')
const bodys = metrics.map((item) => { const bodys = metrics.map((item) => {
const { groups = [] } = item; const { groups = [] } = item;
let newGroups = cloneDeep(groups); let newGroups = cloneDeep(groups);
@ -204,7 +208,7 @@ export default (props) => {
return { return {
cluster_id, cluster_id,
filter, filter,
index_pattern: indices.join(','), index_pattern,
time_field: time_field, time_field: time_field,
...item, ...item,
items: item.items || [], items: item.items || [],
@ -219,6 +223,7 @@ export default (props) => {
if (res) { if (res) {
if (res.some((item) => item.status === 403)) { if (res.some((item) => item.status === 403)) {
setData({ error: 403 }) setData({ error: 403 })
onResultChange && onResultChange()
setLoading(false); setLoading(false);
return; return;
} }
@ -269,8 +274,15 @@ export default (props) => {
} }
} }
setData(newData) setData(newData)
onResultChange && onResultChange(res.map((item) => (
{
...item,
request: item.request ? `GET ${index_pattern}/_search\n${item.request}` : undefined
}
)))
} else { } else {
setData([]) setData([])
onResultChange && onResultChange()
} }
} }
setLoading(false); setLoading(false);

View File

@ -41,7 +41,8 @@ export default (props) => {
queriesBarParams, queriesBarParams,
isFullScreen, isFullScreen,
hideHeader, hideHeader,
displayOptions={} displayOptions={},
onResultChange,
} = props; } = props;
const [cacheRecord, setCacheRecord] = useState(record) const [cacheRecord, setCacheRecord] = useState(record)
@ -252,6 +253,7 @@ export default (props) => {
queriesBarParams={queriesBarParams} queriesBarParams={queriesBarParams}
handleContextMenu={handleContextMenu} handleContextMenu={handleContextMenu}
isFullScreen={isFullScreen} isFullScreen={isFullScreen}
onResultChange={onResultChange}
/> />
</Spin> </Spin>
<WidgetConfigDrawer <WidgetConfigDrawer

View File

@ -1,17 +1,20 @@
import { useEffect, useMemo, useState } from "react" import { useEffect, useMemo, useState } from "react"
import Widget from "./Widget" import Widget from "./Widget"
import styles from "./WidgetLoader.less" import styles from "./WidgetLoader.less"
import { Empty, Spin } from "antd" import { Empty, Icon, message, Spin, Tooltip } from "antd"
import request from "@/utils/request" import request from "@/utils/request"
import { generateFilter, mergeFilters } from "./components/QueriesBar/generate_filters" import { generateFilter, mergeFilters } from "./components/QueriesBar/generate_filters"
import { calculateBounds } from "@/components/vendor/data/common/query/timefilter"; import { calculateBounds } from "@/components/vendor/data/common/query/timefilter";
import moment from "moment" import moment from "moment"
import { getTimezone } from "@/utils/utils" import { getTimezone } from "@/utils/utils"
import { CopyToClipboard } from "react-copy-to-clipboard";
import { formatMessage } from "umi/locale";
export const WidgetRender = (props) => { export const WidgetRender = (props) => {
const { widget, range, query, queryParams = {}, highlightRange = {}, refresh } = props; const { widget, range, query, queryParams = {}, highlightRange = {}, refresh, showCopy = true } = props;
const [globalRangeCache, setGlobalRangeCache] = useState() const [globalRangeCache, setGlobalRangeCache] = useState()
const [requests, setRequests] = useState([])
const filters = useMemo(() => { const filters = useMemo(() => {
const newFilters = [] const newFilters = []
@ -41,6 +44,18 @@ export const WidgetRender = (props) => {
return ( return (
widget ? ( widget ? (
<div className={styles.content}>
{
showCopy && requests.length > 0 && (
<CopyToClipboard text={requests.join('\n')}>
<Tooltip placement="left" title={formatMessage({id: "cluster.metrics.request.copy"})}>
<div className={styles.copy} onClick={() => message.success(formatMessage({id: "cluster.metrics.request.copy.success"}))}>
<Icon type="copy" />
</div>
</Tooltip>
</CopyToClipboard>
)
}
<Widget <Widget
record={widget} record={widget}
globalQueries={{ globalQueries={{
@ -66,7 +81,11 @@ export const WidgetRender = (props) => {
clusterList={[]} clusterList={[]}
highlightRange={highlightRange} highlightRange={highlightRange}
onHighlightRangeChange={() => {}} onHighlightRangeChange={() => {}}
onResultChange={(res) => {
setRequests(Array.isArray(res) ? res.filter((item) => !!item.request).map((item) => item.request) : [])
}}
/> />
</div>
) : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} /> ) : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
) )
} }

View File

@ -11,3 +11,50 @@
} }
} }
} }
.content {
width: 100%;
height: 100%;
position: relative;
.copy {
position: absolute;
display: none;
right: 0px;
top: 0px;
width: 24px;
height: 24px;
border-radius: 4px;
line-height: 24px;
text-align: center;
font-size: 16px;
box-shadow: rgba(0, 0, 0, 0.16) 0px 0px 5px 0px;
background: #fff;
z-index: 11;
cursor: pointer;
color: rgba(0, 0, 0, 0.45);
transition: all 0.3s ease;
&:hover {
color: #1890ff;
box-shadow: rgba(0, 0, 0, 0.3) 0px 0px 5px 0px;
}
}
&:hover {
.copy {
display: block;
}
}
}
.copyTooltip {
:global {
.ant-tooltip-inner {
width: 100px;
}
}
}

View File

@ -179,11 +179,11 @@ class ClusterForm extends React.Component {
agent_credential_id: agent_credential_id:
values.agent_credential_id !== MANUAL_VALUE && isAgentMode values.agent_credential_id !== MANUAL_VALUE && isAgentMode
? values.agent_credential_id ? values.agent_credential_id
: agent_credential_id, : undefined,
agent_basic_auth: isAgentMode ? { agent_basic_auth: {
username: values.agent_username, username: values.agent_username,
password: values.agent_password, password: values.agent_password,
} : agent_basic_auth, },
metric_collection_mode: values.metric_collection_mode || 'agentless', metric_collection_mode: values.metric_collection_mode || 'agentless',
description: values.description, description: values.description,
@ -204,6 +204,7 @@ class ClusterForm extends React.Component {
if (this.clusterUUID) { if (this.clusterUUID) {
newVals.cluster_uuid = this.clusterUUID; newVals.cluster_uuid = this.clusterUUID;
} }
if (clusterConfig.editMode === "NEW") { if (clusterConfig.editMode === "NEW") {
dispatch({ dispatch({
type: "clusterConfig/addCluster", type: "clusterConfig/addCluster",

View File

@ -90,7 +90,7 @@ export default {
}, },
*updateCluster({ payload }, { call, put, select }) { *updateCluster({ payload }, { call, put, select }) {
let res = yield call(updateClusterConfig, payload); let res = yield call(updateClusterConfig, payload);
if (res.error) { if (res?.error) {
return false; return false;
} }
let { data } = yield select((state) => state.clusterConfig); let { data } = yield select((state) => state.clusterConfig);