Compare commits
24 Commits
v1.28.2
...
fix_gw_age
Author | SHA1 | Date |
---|---|---|
|
5cc4ea9ef1 | |
|
a7ff1c6220 | |
|
9a9896c2f2 | |
|
785f2ede57 | |
|
9d5734b61e | |
|
a0d28fada9 | |
|
d851be6a38 | |
|
aa67bf7c80 | |
|
6c9e8d28c7 | |
|
ca52a5b00d | |
|
1e2f8c2520 | |
|
df33fa006b | |
|
183ebf037c | |
|
6ff2d72ff1 | |
|
1ecdbb3e59 | |
|
78cdd44e9c | |
|
932a2a46e1 | |
|
1cd1f98af4 | |
|
8452d8ef3e | |
|
1e601f259b | |
|
06f7d3bc77 | |
|
a092dd7cb1 | |
|
3f7c32d4de | |
|
f679a57fb2 |
|
@ -1,22 +1,22 @@
|
||||||
path.data: data
|
path.data: data
|
||||||
path.logs: log
|
path.logs: log
|
||||||
|
|
||||||
allow_multi_instance: true
|
allow_multi_instance: false
|
||||||
configs.auto_reload: false
|
configs.auto_reload: true
|
||||||
|
|
||||||
entry:
|
entry:
|
||||||
- name: my_es_entry
|
- name: agent_es_entry
|
||||||
enabled: true
|
enabled: true
|
||||||
router: my_router
|
router: agent_metrics_router
|
||||||
max_concurrency: 200000
|
max_concurrency: 200000
|
||||||
network:
|
network:
|
||||||
binding: 0.0.0.0:8081
|
binding: 0.0.0.0:8765
|
||||||
# tls: #for mTLS connection with config servers
|
tls: #for mTLS connection with config servers
|
||||||
# enabled: true
|
enabled: true
|
||||||
# ca_file: /xxx/ca.crt
|
# ca_file: /xxx/ca.crt
|
||||||
# cert_file: /xxx/server.crt
|
# cert_file: /xxx/server.crt
|
||||||
# key_file: /xxx/server.key
|
# key_file: /xxx/server.key
|
||||||
# skip_insecure_verify: false
|
skip_insecure_verify: false
|
||||||
|
|
||||||
flow:
|
flow:
|
||||||
- name: deny_flow
|
- name: deny_flow
|
||||||
|
@ -28,7 +28,7 @@ flow:
|
||||||
filter:
|
filter:
|
||||||
- basic_auth:
|
- basic_auth:
|
||||||
valid_users:
|
valid_users:
|
||||||
ingest: n
|
$[[SETUP_AGENT_USERNAME]]: $[[SETUP_AGENT_PASSWORD]]
|
||||||
- rewrite_to_bulk:
|
- rewrite_to_bulk:
|
||||||
type_removed: false
|
type_removed: false
|
||||||
- bulk_request_mutate:
|
- bulk_request_mutate:
|
||||||
|
@ -50,7 +50,7 @@ flow:
|
||||||
fix_null_id: true
|
fix_null_id: true
|
||||||
|
|
||||||
router:
|
router:
|
||||||
- name: my_router
|
- name: agent_metrics_router
|
||||||
default_flow: deny_flow
|
default_flow: deny_flow
|
||||||
rules:
|
rules:
|
||||||
- method:
|
- method:
|
||||||
|
@ -65,8 +65,8 @@ elasticsearch:
|
||||||
- name: prod
|
- name: prod
|
||||||
enabled: true
|
enabled: true
|
||||||
basic_auth:
|
basic_auth:
|
||||||
username: ingest
|
username: $[[SETUP_AGENT_USERNAME]]
|
||||||
password: password
|
password: $[[SETUP_AGENT_PASSWORD]]
|
||||||
endpoints: $[[SETUP_ENDPOINTS]]
|
endpoints: $[[SETUP_ENDPOINTS]]
|
||||||
|
|
||||||
pipeline:
|
pipeline:
|
||||||
|
|
|
@ -34,6 +34,7 @@ pipeline:
|
||||||
labels:
|
labels:
|
||||||
cluster_id: $[[CLUSTER_ID]]
|
cluster_id: $[[CLUSTER_ID]]
|
||||||
cluster_uuid: $[[CLUSTER_UUID]]
|
cluster_uuid: $[[CLUSTER_UUID]]
|
||||||
|
cluster_name: $[[CLUSTER_NAME]]
|
||||||
when:
|
when:
|
||||||
cluster_available: ["$[[TASK_ID]]"]
|
cluster_available: ["$[[TASK_ID]]"]
|
||||||
|
|
||||||
|
@ -49,6 +50,7 @@ pipeline:
|
||||||
labels:
|
labels:
|
||||||
cluster_id: $[[CLUSTER_ID]]
|
cluster_id: $[[CLUSTER_ID]]
|
||||||
cluster_uuid: $[[CLUSTER_UUID]]
|
cluster_uuid: $[[CLUSTER_UUID]]
|
||||||
|
cluster_name: $[[CLUSTER_NAME]]
|
||||||
logs_path: $[[NODE_LOGS_PATH]]
|
logs_path: $[[NODE_LOGS_PATH]]
|
||||||
queue_name: logs
|
queue_name: logs
|
||||||
when:
|
when:
|
||||||
|
|
|
@ -28,6 +28,19 @@ PUT _template/$[[SETUP_TEMPLATE_NAME]]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mappings": {
|
"mappings": {
|
||||||
|
"properties": {
|
||||||
|
"metadata": {
|
||||||
|
"properties": {
|
||||||
|
"labels": {
|
||||||
|
"properties": {
|
||||||
|
"cluster_id": {
|
||||||
|
"type": "keyword"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dynamic_templates": [
|
"dynamic_templates": [
|
||||||
{
|
{
|
||||||
"strings": {
|
"strings": {
|
||||||
|
@ -368,6 +381,12 @@ PUT _template/$[[SETUP_INDEX_PREFIX]]alert-history-rollover
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mappings" : {
|
"mappings" : {
|
||||||
|
"properties" : {
|
||||||
|
"condition_result" : {
|
||||||
|
"type" : "object",
|
||||||
|
"enabled" : false
|
||||||
|
}
|
||||||
|
},
|
||||||
"dynamic_templates" : [
|
"dynamic_templates" : [
|
||||||
{
|
{
|
||||||
"strings" : {
|
"strings" : {
|
||||||
|
|
|
@ -27,6 +27,19 @@ PUT _template/$[[SETUP_TEMPLATE_NAME]]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mappings": {
|
"mappings": {
|
||||||
|
"properties": {
|
||||||
|
"metadata": {
|
||||||
|
"properties": {
|
||||||
|
"labels": {
|
||||||
|
"properties": {
|
||||||
|
"cluster_id": {
|
||||||
|
"type": "keyword"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dynamic_templates": [
|
"dynamic_templates": [
|
||||||
{
|
{
|
||||||
"strings": {
|
"strings": {
|
||||||
|
|
|
@ -27,6 +27,19 @@ PUT _template/$[[SETUP_TEMPLATE_NAME]]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mappings": {
|
"mappings": {
|
||||||
|
"properties": {
|
||||||
|
"metadata": {
|
||||||
|
"properties": {
|
||||||
|
"labels": {
|
||||||
|
"properties": {
|
||||||
|
"cluster_id": {
|
||||||
|
"type": "keyword"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dynamic_templates": [
|
"dynamic_templates": [
|
||||||
{
|
{
|
||||||
"strings": {
|
"strings": {
|
||||||
|
|
|
@ -104,6 +104,7 @@ security:
|
||||||
# group_attribute: "ou"
|
# group_attribute: "ou"
|
||||||
# bypass_api_key: true
|
# bypass_api_key: true
|
||||||
# cache_ttl: "10s"
|
# cache_ttl: "10s"
|
||||||
|
# default_roles: ["ReadonlyUI","DATA"] #default for all ldap users if no specify roles was defined
|
||||||
# role_mapping:
|
# role_mapping:
|
||||||
# group:
|
# group:
|
||||||
# superheros: [ "Administrator" ]
|
# superheros: [ "Administrator" ]
|
||||||
|
@ -118,6 +119,7 @@ security:
|
||||||
# base_dn: "dc=example,dc=com"
|
# base_dn: "dc=example,dc=com"
|
||||||
# user_filter: "(uid=%s)"
|
# user_filter: "(uid=%s)"
|
||||||
# cache_ttl: "10s"
|
# cache_ttl: "10s"
|
||||||
|
# default_roles: ["ReadonlyUI","DATA"] #default for all ldap users if no specify roles was defined
|
||||||
# role_mapping:
|
# role_mapping:
|
||||||
# uid:
|
# uid:
|
||||||
# tesla: [ "readonly","data" ]
|
# tesla: [ "readonly","data" ]
|
||||||
|
|
|
@ -7,14 +7,43 @@ title: "Release Notes"
|
||||||
|
|
||||||
Information about release notes of INFINI Console is provided here.
|
Information about release notes of INFINI Console is provided here.
|
||||||
|
|
||||||
|
## Latest (In development)
|
||||||
|
|
||||||
|
### Breaking changes
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Add Logs to Monitor (cluster, node)
|
||||||
|
|
||||||
|
### Bug fix
|
||||||
|
- Fixed the error when querying empty metric data (#144)
|
||||||
|
- Fixed empty host when setup step finishes (#147)
|
||||||
|
- Fixed the error of obtaining suggestions of field's value in discover (#151)
|
||||||
|
- Fixed the wrong display of heatmap's data in alerting message (#157)
|
||||||
|
- Fixed Devtools `_sql` support for elasticsearch 6.x (#158)
|
||||||
|
- Fixed audit log default sorting across pagination (#161)
|
||||||
|
- Fixed mapping type conflict error (#164)
|
||||||
|
- Fixed `Gateway` template config for mTLS(#166)
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
- Update agent config with cluster name (#148)
|
||||||
|
- Optimize UI of histogram and datepicker in discover (#151)
|
||||||
|
- Support viewing logs for cluster, node, index health change events (#150)
|
||||||
|
- Enhance LDAP authentication logging (#156)
|
||||||
|
- Optimize UI for copying metric requests (#155)
|
||||||
|
- Enhance deletion tips by adding cluster info for indices
|
||||||
|
- Support clearing offline agent instances (#165)
|
||||||
|
|
||||||
## 1.28.2 (2025-02-15)
|
## 1.28.2 (2025-02-15)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
- Support alerts based on bucket diff state (#119)
|
- Support alerts based on bucket diff state (#119)
|
||||||
- Add rollup ilm when use Easysearch (#128)
|
- Add rollup ilm when use Easysearch (#128)
|
||||||
|
- Log activity for cluster metric collection mode changes (#152)
|
||||||
|
|
||||||
### Bug fix
|
### Bug fix
|
||||||
- Fixed missing data when processing multiple time series in a group with insight data API (#127)
|
- Fixed missing data when processing multiple time series in a group with insight data API (#127)
|
||||||
|
- Fixed incorrect node health change activity logging (#154)
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
|
|
||||||
|
|
|
@ -7,14 +7,43 @@ title: "版本历史"
|
||||||
|
|
||||||
这里是 INFINI Console 历史版本发布的相关说明。
|
这里是 INFINI Console 历史版本发布的相关说明。
|
||||||
|
|
||||||
|
## Latest (In development)
|
||||||
|
|
||||||
|
### Breaking changes
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- 监控(集群、节点)新增日志查询
|
||||||
|
|
||||||
|
### Bug fix
|
||||||
|
- 修复指标数据为空时的查询错误 (#144)
|
||||||
|
- 修复初始化结束步骤中主机显示为错误的问题 (#147)
|
||||||
|
- 修复数据探索中获取字段值建议的错误 (#151)
|
||||||
|
- 修复告警消息热图数据显示错误的问题 (#157)
|
||||||
|
- 修复开发工具 `_sql` 查询支撑 Elasticsearch 6.x 版本 (#158)
|
||||||
|
- 修复审计日志默认排序翻页之后丢失的问题 (#161)
|
||||||
|
- 修复 `Mapping` 冲突问题 (#161)
|
||||||
|
- 修复配置文件模板中 `Gateway` mTLS 配置(#166)
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
- 优化下发给 Agent 的配置,增加集群名称 (#148)
|
||||||
|
- 优化柱状图和时间选择器的 UI (#151)
|
||||||
|
- 集群,节点,索引健康状态变更支持查看日志 (#150)
|
||||||
|
- 增强 LDAP 身份验证的日志记录 (#156)
|
||||||
|
- 优化监控报表里拷贝指标请求的 UI (#155)
|
||||||
|
- 删除索引提示增加集群信息 (#162)
|
||||||
|
|
||||||
## 1.28.2 (2025-02-15)
|
## 1.28.2 (2025-02-15)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
- 告警功能支持根据桶之间文档数差值和内容差异告警 (#119)
|
- 告警功能支持根据桶之间文档数差值和内容差异告警 (#119)
|
||||||
- 当使用 Easysearch 存储指标时,增加 Rollup 索引生命周期 (#128)
|
- 当使用 Easysearch 存储指标时,增加 Rollup 索引生命周期 (#128)
|
||||||
|
- 增加集群指标采集模式变更事件 (#152)
|
||||||
|
- 支持清理离线 Agent 实例(#165)
|
||||||
|
|
||||||
### Bug fix
|
### Bug fix
|
||||||
- 修复 Insight API 处理多时间序列数据时数据丢失的问题 (#127)
|
- 修复 Insight API 处理多时间序列数据时数据丢失的问题 (#127)
|
||||||
|
- 修复错误的节点健康状态变更事件 (#154)
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
|
|
||||||
|
@ -22,8 +51,8 @@ title: "版本历史"
|
||||||
- 在注册 Agent 中新增 Agent 凭据设置
|
- 在注册 Agent 中新增 Agent 凭据设置
|
||||||
- 在集群编辑中新增采集模式
|
- 在集群编辑中新增采集模式
|
||||||
- 当使用 Easysearch 存储指标时,自动为系统集群创建 Agent 指标写入最小权限用户 (#120)
|
- 当使用 Easysearch 存储指标时,自动为系统集群创建 Agent 指标写入最小权限用户 (#120)
|
||||||
- 修复 LDAP 用户映射增加默认权限组 (#114) (#130)
|
- 优化 LDAP 用户映射增加默认权限组 (#114) (#130)
|
||||||
- Agent 连接 Easysearch 的配置信息中增加 `version` 和 `distribution` 来解决启动时退出问题 (#131)
|
- 优化 Agent 连接 Easysearch 的配置信息中增加 `version` 和 `distribution` 来解决启动时退出问题 (#131)
|
||||||
|
|
||||||
## 1.28.1 (2025-01-24)
|
## 1.28.1 (2025-01-24)
|
||||||
|
|
||||||
|
|
|
@ -199,11 +199,13 @@ func getAgentIngestConfigs(instance string, items map[string]BindingItem) (strin
|
||||||
var password = ""
|
var password = ""
|
||||||
var version = ""
|
var version = ""
|
||||||
var distribution = ""
|
var distribution = ""
|
||||||
|
var clusterName = ""
|
||||||
|
|
||||||
if metadata.Config != nil {
|
if metadata.Config != nil {
|
||||||
|
|
||||||
version = metadata.Config.Version
|
version = metadata.Config.Version
|
||||||
distribution = metadata.Config.Distribution
|
distribution = metadata.Config.Distribution
|
||||||
|
clusterName = metadata.Config.Name
|
||||||
|
|
||||||
if metadata.Config.AgentCredentialID != "" {
|
if metadata.Config.AgentCredentialID != "" {
|
||||||
credential, err := common2.GetCredential(metadata.Config.AgentCredentialID)
|
credential, err := common2.GetCredential(metadata.Config.AgentCredentialID)
|
||||||
|
@ -250,6 +252,7 @@ func getAgentIngestConfigs(instance string, items map[string]BindingItem) (strin
|
||||||
"variable:\n "+
|
"variable:\n "+
|
||||||
"TASK_ID: %v\n "+
|
"TASK_ID: %v\n "+
|
||||||
"CLUSTER_ID: %v\n "+
|
"CLUSTER_ID: %v\n "+
|
||||||
|
"CLUSTER_NAME: %v\n "+
|
||||||
"CLUSTER_UUID: %v\n "+
|
"CLUSTER_UUID: %v\n "+
|
||||||
"NODE_UUID: %v\n "+
|
"NODE_UUID: %v\n "+
|
||||||
"CLUSTER_VERSION: %v\n "+
|
"CLUSTER_VERSION: %v\n "+
|
||||||
|
@ -260,7 +263,7 @@ func getAgentIngestConfigs(instance string, items map[string]BindingItem) (strin
|
||||||
"CLUSTER_LEVEL_TASKS_ENABLED: %v\n "+
|
"CLUSTER_LEVEL_TASKS_ENABLED: %v\n "+
|
||||||
"NODE_LEVEL_TASKS_ENABLED: %v\n "+
|
"NODE_LEVEL_TASKS_ENABLED: %v\n "+
|
||||||
"NODE_LOGS_PATH: \"%v\"\n\n\n", taskID, taskID,
|
"NODE_LOGS_PATH: \"%v\"\n\n\n", taskID, taskID,
|
||||||
v.ClusterID, v.ClusterUUID, v.NodeUUID, version, distribution, nodeEndPoint, username, password, clusterLevelEnabled, nodeLevelEnabled, pathLogs)))
|
v.ClusterID, clusterName, v.ClusterUUID, v.NodeUUID, version, distribution, nodeEndPoint, username, password, clusterLevelEnabled, nodeLevelEnabled, pathLogs)))
|
||||||
}
|
}
|
||||||
|
|
||||||
hash := util.MD5digest(buffer.String())
|
hash := util.MD5digest(buffer.String())
|
||||||
|
|
|
@ -27,6 +27,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"infini.sh/framework/core/queue"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -107,6 +108,9 @@ func (h *APIHandler) HandleCreateClusterAction(w http.ResponseWriter, req *http.
|
||||||
if conf.Distribution == "" {
|
if conf.Distribution == "" {
|
||||||
conf.Distribution = elastic.Elasticsearch
|
conf.Distribution = elastic.Elasticsearch
|
||||||
}
|
}
|
||||||
|
if conf.MetricCollectionMode == "" {
|
||||||
|
conf.MetricCollectionMode = elastic.ModeAgentless
|
||||||
|
}
|
||||||
err = orm.Create(ctx, conf)
|
err = orm.Create(ctx, conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
|
@ -183,6 +187,7 @@ func (h *APIHandler) HandleUpdateClusterAction(w http.ResponseWriter, req *http.
|
||||||
h.Error404(w)
|
h.Error404(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
var oldCollectionMode = originConf.MetricCollectionMode
|
||||||
buf := util.MustToJSONBytes(originConf)
|
buf := util.MustToJSONBytes(originConf)
|
||||||
source := map[string]interface{}{}
|
source := map[string]interface{}{}
|
||||||
util.MustFromJSONBytes(buf, &source)
|
util.MustFromJSONBytes(buf, &source)
|
||||||
|
@ -255,7 +260,10 @@ func (h *APIHandler) HandleUpdateClusterAction(w http.ResponseWriter, req *http.
|
||||||
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// record cluster metric collection mode change activity
|
||||||
|
if oldCollectionMode != newConf.MetricCollectionMode {
|
||||||
|
recordCollectionModeChangeActivity(newConf.ID, newConf.Name, oldCollectionMode, newConf.MetricCollectionMode)
|
||||||
|
}
|
||||||
basicAuth, err := common.GetBasicAuth(newConf)
|
basicAuth, err := common.GetBasicAuth(newConf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
@ -273,6 +281,47 @@ func (h *APIHandler) HandleUpdateClusterAction(w http.ResponseWriter, req *http.
|
||||||
h.WriteUpdatedOKJSON(w, id)
|
h.WriteUpdatedOKJSON(w, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func recordCollectionModeChangeActivity(clusterID, clusterName, oldMode, newMode string) {
|
||||||
|
activityInfo := &event.Activity{
|
||||||
|
ID: util.GetUUID(),
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Metadata: event.ActivityMetadata{
|
||||||
|
Category: "elasticsearch",
|
||||||
|
Group: "platform",
|
||||||
|
Name: "metric_collection_mode_change",
|
||||||
|
Type: "update",
|
||||||
|
Labels: util.MapStr{
|
||||||
|
"cluster_id": clusterID,
|
||||||
|
"cluster_name": clusterName,
|
||||||
|
"from": oldMode,
|
||||||
|
"to": newMode,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
queueConfig := queue.GetOrInitConfig("platform##activities")
|
||||||
|
if queueConfig.Labels == nil {
|
||||||
|
queueConfig.ReplaceLabels(util.MapStr{
|
||||||
|
"type": "platform",
|
||||||
|
"name": "activity",
|
||||||
|
"category": "elasticsearch",
|
||||||
|
"activity": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
err := queue.Push(queueConfig, util.MustToJSONBytes(event.Event{
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Metadata: event.EventMetadata{
|
||||||
|
Category: "elasticsearch",
|
||||||
|
Name: "activity",
|
||||||
|
},
|
||||||
|
Fields: util.MapStr{
|
||||||
|
"activity": activityInfo,
|
||||||
|
}}))
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *APIHandler) HandleDeleteClusterAction(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
|
func (h *APIHandler) HandleDeleteClusterAction(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
|
||||||
resBody := map[string]interface{}{}
|
resBody := map[string]interface{}{}
|
||||||
id := ps.MustGetParameter("id")
|
id := ps.MustGetParameter("id")
|
||||||
|
|
|
@ -80,6 +80,7 @@ func (h *APIHandler) HandleProxyAction(w http.ResponseWriter, req *http.Request,
|
||||||
}
|
}
|
||||||
if strings.Trim(newURL.Path, "/") == "_sql" {
|
if strings.Trim(newURL.Path, "/") == "_sql" {
|
||||||
distribution := esClient.GetVersion().Distribution
|
distribution := esClient.GetVersion().Distribution
|
||||||
|
version := esClient.GetVersion().Number
|
||||||
indexName, err := rewriteTableNamesOfSqlRequest(req, distribution)
|
indexName, err := rewriteTableNamesOfSqlRequest(req, distribution)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
@ -92,6 +93,15 @@ func (h *APIHandler) HandleProxyAction(w http.ResponseWriter, req *http.Request,
|
||||||
q, _ := url.ParseQuery(newURL.RawQuery)
|
q, _ := url.ParseQuery(newURL.RawQuery)
|
||||||
hasFormat := q.Has("format")
|
hasFormat := q.Has("format")
|
||||||
switch distribution {
|
switch distribution {
|
||||||
|
case elastic.Elasticsearch:
|
||||||
|
if !hasFormat {
|
||||||
|
q.Add("format", "txt")
|
||||||
|
}
|
||||||
|
if large, _ := util.VersionCompare(version, "7.0.0"); large > 0 {
|
||||||
|
path = "_sql?" + q.Encode()
|
||||||
|
} else {
|
||||||
|
path = "_xpack/_sql?" + q.Encode()
|
||||||
|
}
|
||||||
case elastic.Opensearch:
|
case elastic.Opensearch:
|
||||||
path = "_plugins/_sql?format=raw"
|
path = "_plugins/_sql?format=raw"
|
||||||
case elastic.Easysearch:
|
case elastic.Easysearch:
|
||||||
|
|
|
@ -82,6 +82,9 @@ func (r *LDAPRealm) mapLDAPRoles(authInfo auth.Info) []string {
|
||||||
}
|
}
|
||||||
|
|
||||||
//map group
|
//map group
|
||||||
|
if len(authInfo.GetGroups()) == 0 {
|
||||||
|
log.Debugf("LDAP uid: %v, user: %v, group: %v", uid, authInfo, authInfo.GetGroups())
|
||||||
|
}
|
||||||
for _, roleName := range authInfo.GetGroups() {
|
for _, roleName := range authInfo.GetGroups() {
|
||||||
newRoles, ok := r.config.RoleMapping.Group[roleName]
|
newRoles, ok := r.config.RoleMapping.Group[roleName]
|
||||||
if ok {
|
if ok {
|
||||||
|
|
|
@ -77,9 +77,9 @@ func Init(config *config.Config) {
|
||||||
|
|
||||||
func Authenticate(username, password string) (bool, *rbac.User, error) {
|
func Authenticate(username, password string) (bool, *rbac.User, error) {
|
||||||
|
|
||||||
for i, realm := range realms {
|
for _, realm := range realms {
|
||||||
ok, user, err := realm.Authenticate(username, password)
|
ok, user, err := realm.Authenticate(username, password)
|
||||||
log.Debugf("authenticate result: %v, user: %v, err: %v, realm: %v", ok, user, err, i)
|
log.Debugf("authenticate result: %v, user: %v, err: %v, realm: %v", ok, user, err, realm.GetType())
|
||||||
if ok && user != nil && err == nil {
|
if ok && user != nil && err == nil {
|
||||||
return true, user, nil
|
return true, user, nil
|
||||||
}
|
}
|
||||||
|
@ -92,14 +92,14 @@ func Authenticate(username, password string) (bool, *rbac.User, error) {
|
||||||
|
|
||||||
func Authorize(user *rbac.User) (bool, error) {
|
func Authorize(user *rbac.User) (bool, error) {
|
||||||
|
|
||||||
for i, realm := range realms {
|
for _, realm := range realms {
|
||||||
//skip if not the same auth provider, TODO: support cross-provider authorization
|
//skip if not the same auth provider, TODO: support cross-provider authorization
|
||||||
if user.AuthProvider != realm.GetType() {
|
if user.AuthProvider != realm.GetType() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
ok, err := realm.Authorize(user)
|
ok, err := realm.Authorize(user)
|
||||||
log.Debugf("authorize result: %v, user: %v, err: %v, realm: %v", ok, user, err, i)
|
log.Debugf("authorize result: %v, user: %v, err: %v, realm: %v", ok, user, err, realm.GetType())
|
||||||
if ok && err == nil {
|
if ok && err == nil {
|
||||||
//return on any success, TODO, maybe merge all roles and privileges from all realms
|
//return on any success, TODO, maybe merge all roles and privileges from all realms
|
||||||
return true, nil
|
return true, nil
|
||||||
|
|
|
@ -132,7 +132,7 @@ func (processor *MetadataProcessor) HandleUnknownNodeStatus(ev []byte) error {
|
||||||
}
|
}
|
||||||
esClient := elastic.GetClient(processor.config.Elasticsearch)
|
esClient := elastic.GetClient(processor.config.Elasticsearch)
|
||||||
queryDslTpl := `{"script": {
|
queryDslTpl := `{"script": {
|
||||||
"source": "ctx._source.metadata.labels.status='unavailable'",
|
"source": "ctx._source.metadata.labels.status='unknown'",
|
||||||
"lang": "painless"
|
"lang": "painless"
|
||||||
},
|
},
|
||||||
"query": {
|
"query": {
|
||||||
|
|
|
@ -30,6 +30,9 @@ package server
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"infini.sh/framework/core/event"
|
||||||
|
"infini.sh/framework/core/global"
|
||||||
|
"infini.sh/framework/core/task"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -76,6 +79,8 @@ func init() {
|
||||||
|
|
||||||
//try to connect to instance
|
//try to connect to instance
|
||||||
api.HandleAPIMethod(api.POST, "/instance/try_connect", handler.RequireLogin(handler.tryConnect))
|
api.HandleAPIMethod(api.POST, "/instance/try_connect", handler.RequireLogin(handler.tryConnect))
|
||||||
|
//clear instance that is not alive in 7 days
|
||||||
|
api.HandleAPIMethod(api.POST, "/instance/_clear", handler.RequirePermission(handler.clearInstance, enum.PermissionGatewayInstanceWrite))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,18 +90,20 @@ func (h APIHandler) registerInstance(w http.ResponseWriter, req *http.Request, p
|
||||||
err := h.DecodeJSON(req, obj)
|
err := h.DecodeJSON(req, obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if obj.Endpoint == "" {
|
||||||
|
h.WriteError(w, "empty endpoint", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
oldInst := &model.Instance{}
|
oldInst := &model.Instance{}
|
||||||
oldInst.ID = obj.ID
|
oldInst.ID = obj.ID
|
||||||
exists, err := orm.Get(oldInst)
|
exists, err := orm.Get(oldInst)
|
||||||
if exists {
|
if exists {
|
||||||
errMsg := fmt.Sprintf("agent [%s] already exists", obj.ID)
|
obj.Created = oldInst.Created
|
||||||
h.WriteError(w, errMsg, http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
err = orm.Save(nil, obj)
|
||||||
err = orm.Create(nil, obj)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
h.WriteError(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
@ -369,6 +376,168 @@ func (h *APIHandler) getInstanceStatus(w http.ResponseWriter, req *http.Request,
|
||||||
}
|
}
|
||||||
h.WriteJSON(w, result, http.StatusOK)
|
h.WriteJSON(w, result, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
func (h *APIHandler) clearInstance(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
|
||||||
|
appName := h.GetParameterOrDefault(req, "app_name", "")
|
||||||
|
task.RunWithinGroup("clear_instance", func(ctx context.Context) error {
|
||||||
|
err := h.clearInstanceByAppName(appName)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
h.WriteAckOKJSON(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *APIHandler) clearInstanceByAppName(appName string) error {
|
||||||
|
var (
|
||||||
|
size = 100
|
||||||
|
from = 0
|
||||||
|
)
|
||||||
|
// Paginated query for all running instances
|
||||||
|
q := orm.Query{
|
||||||
|
Size: size,
|
||||||
|
From: from,
|
||||||
|
}
|
||||||
|
if appName != "" {
|
||||||
|
q.Conds = orm.And(
|
||||||
|
orm.Eq("application.name", appName),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
q.AddSort("created", orm.ASC)
|
||||||
|
insts := []model.Instance{}
|
||||||
|
var (
|
||||||
|
instanceIDs []string
|
||||||
|
toRemoveIDs []string
|
||||||
|
instsCache = map[string]*model.Instance{}
|
||||||
|
)
|
||||||
|
client := elastic2.GetClient(global.MustLookupString(elastic2.GlobalSystemElasticsearchID))
|
||||||
|
for {
|
||||||
|
err, _ := orm.SearchWithJSONMapper(&insts, &q)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, inst := range insts {
|
||||||
|
instanceIDs = append(instanceIDs, inst.ID)
|
||||||
|
instsCache[inst.ID] = &inst
|
||||||
|
}
|
||||||
|
if len(instanceIDs) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
aliveInstanceIDs, err := getAliveInstanceIDs(client, instanceIDs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, instanceID := range instanceIDs {
|
||||||
|
if _, ok := aliveInstanceIDs[instanceID]; !ok {
|
||||||
|
toRemoveIDs = append(toRemoveIDs, instanceID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(toRemoveIDs) > 0 {
|
||||||
|
// Use the same slice to avoid extra allocation
|
||||||
|
filteredIDs := toRemoveIDs[:0]
|
||||||
|
// check whether the instance is still online
|
||||||
|
for _, instanceID := range toRemoveIDs {
|
||||||
|
if inst, ok := instsCache[instanceID]; ok {
|
||||||
|
_, err = h.getInstanceInfo(inst.Endpoint, inst.BasicAuth)
|
||||||
|
if err == nil {
|
||||||
|
// Skip online instance, do not append to filtered list
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Keep only offline instances
|
||||||
|
filteredIDs = append(filteredIDs, instanceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign back after filtering
|
||||||
|
toRemoveIDs = filteredIDs
|
||||||
|
query := util.MapStr{
|
||||||
|
"query": util.MapStr{
|
||||||
|
"terms": util.MapStr{
|
||||||
|
"id": toRemoveIDs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// remove instances
|
||||||
|
err = orm.DeleteBy(model.Instance{}, util.MustToJSONBytes(query))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete instance: %w", err)
|
||||||
|
}
|
||||||
|
// remove instance related data
|
||||||
|
query = util.MapStr{
|
||||||
|
"query": util.MapStr{
|
||||||
|
"terms": util.MapStr{
|
||||||
|
"metadata.labels.agent_id": toRemoveIDs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = orm.DeleteBy(model.Setting{}, util.MustToJSONBytes(query))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit loop when the number of returned records is less than the page size
|
||||||
|
if len(insts) <= size {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Reset instance state for the next iteration
|
||||||
|
insts = []model.Instance{}
|
||||||
|
toRemoveIDs = nil
|
||||||
|
instsCache = make(map[string]*model.Instance)
|
||||||
|
q.From += size
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAliveInstanceIDs(client elastic2.API, instanceIDs []string) (map[string]struct{}, error) {
|
||||||
|
query := util.MapStr{
|
||||||
|
"size": 0,
|
||||||
|
"query": util.MapStr{
|
||||||
|
"bool": util.MapStr{
|
||||||
|
"must": []util.MapStr{
|
||||||
|
{
|
||||||
|
"terms": util.MapStr{
|
||||||
|
"agent.id": instanceIDs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"range": util.MapStr{
|
||||||
|
"timestamp": util.MapStr{
|
||||||
|
"gt": "now-7d",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"aggs": util.MapStr{
|
||||||
|
"grp_agent_id": util.MapStr{
|
||||||
|
"terms": util.MapStr{
|
||||||
|
"field": "agent.id",
|
||||||
|
},
|
||||||
|
"aggs": util.MapStr{
|
||||||
|
"count": util.MapStr{
|
||||||
|
"value_count": util.MapStr{
|
||||||
|
"field": "agent.id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
queryDSL := util.MustToJSONBytes(query)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||||
|
defer cancel()
|
||||||
|
response, err := client.QueryDSL(ctx, orm.GetWildcardIndexName(event.Event{}), nil, queryDSL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ret := map[string]struct{}{}
|
||||||
|
for _, bk := range response.Aggregations["grp_agent_id"].Buckets {
|
||||||
|
key := bk["key"].(string)
|
||||||
|
if bk["doc_count"].(float64) > 0 {
|
||||||
|
ret[key] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (h *APIHandler) proxy(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
|
func (h *APIHandler) proxy(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
|
||||||
var (
|
var (
|
||||||
|
@ -417,7 +586,7 @@ func (h *APIHandler) getInstanceInfo(endpoint string, basicAuth *model.BasicAuth
|
||||||
obj := &model.Instance{}
|
obj := &model.Instance{}
|
||||||
_, err := ProxyAgentRequest("runtime", endpoint, req1, obj)
|
_, err := ProxyAgentRequest("runtime", endpoint, req1, obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
return obj, err
|
return obj, err
|
||||||
|
|
||||||
|
|
|
@ -215,7 +215,7 @@ const DatePicker = (props) => {
|
||||||
isMinimum ? styles.minimum : ""
|
isMinimum ? styles.minimum : ""
|
||||||
} ${className}`}
|
} ${className}`}
|
||||||
>
|
>
|
||||||
<Button.Group className={styles.RangeBox}>
|
<Button.Group className={styles.RangeBox} style={{ width: onRefresh ? 'calc(100% - 64px)' : 'calc(100% - 32px)'}}>
|
||||||
{!isMinimum && (
|
{!isMinimum && (
|
||||||
<Button
|
<Button
|
||||||
className={`${styles.iconBtn} common-ui-datepicker-backward`}
|
className={`${styles.iconBtn} common-ui-datepicker-backward`}
|
||||||
|
|
|
@ -42,8 +42,8 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-left: 4px !important;
|
margin-left: 4px !important;
|
||||||
.play {
|
.play {
|
||||||
min-width: 30px;
|
min-width: 32px;
|
||||||
max-width: 30px;
|
max-width: 32px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #1890ff;
|
color: #1890ff;
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { formatMessage } from "umi/locale";
|
||||||
import { getDocPathByLang, getWebsitePathByLang } from "@/utils/utils";
|
import { getDocPathByLang, getWebsitePathByLang } from "@/utils/utils";
|
||||||
|
|
||||||
export default ({autoInit = false}) => {
|
export default ({autoInit = false}) => {
|
||||||
const { loading, value } = useFetch(`/instance/_search`);
|
|
||||||
|
|
||||||
const [tokenLoading, setTokenLoading] = useState(false);
|
const [tokenLoading, setTokenLoading] = useState(false);
|
||||||
|
|
||||||
|
@ -18,7 +17,6 @@ export default ({autoInit = false}) => {
|
||||||
|
|
||||||
const fetchTokenInfo = async () => {
|
const fetchTokenInfo = async () => {
|
||||||
setTokenInfo()
|
setTokenInfo()
|
||||||
// if (seletedGateways.length === 0) return;
|
|
||||||
setTokenLoading(true)
|
setTokenLoading(true)
|
||||||
const res = await request('/instance/_generate_install_script', {
|
const res = await request('/instance/_generate_install_script', {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -35,32 +33,10 @@ export default ({autoInit = false}) => {
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const gateways = value?.hits?.hits || []
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Spin spinning={loading || tokenLoading}>
|
<Spin spinning={tokenLoading}>
|
||||||
<div className={styles.installAgent}>
|
<div className={styles.installAgent}>
|
||||||
{/* <Form className={styles.gateway} layout="vertical">
|
|
||||||
<Form.Item label="选择接入网关" required>
|
|
||||||
<Select
|
|
||||||
mode="multiple"
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
onChange={(value) => setSeletedGateways(value)}
|
|
||||||
onBlur={() => fetchTokenInfo()}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
gateways.map((item) => (
|
|
||||||
<Select.Option key={item._source.endpoint}>
|
|
||||||
<span>
|
|
||||||
<span style={{marginRight: 4}}>{item._source.name}</span>
|
|
||||||
<span>[{item._source.endpoint}]</span>
|
|
||||||
</span>
|
|
||||||
</Select.Option>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
</Form> */}
|
|
||||||
{!autoInit && <Button className={styles.gateway} type="primary" onClick={() => fetchTokenInfo()}>
|
{!autoInit && <Button className={styles.gateway} type="primary" onClick={() => fetchTokenInfo()}>
|
||||||
{formatMessage({
|
{formatMessage({
|
||||||
id:"agent.install.label.get_cmd"
|
id:"agent.install.label.get_cmd"
|
||||||
|
|
|
@ -119,9 +119,10 @@ const Monitor = (props) => {
|
||||||
setParam({ ...param, timeRange: state.timeRange, timeInterval: state.timeInterval, timeout: state.timeout });
|
setParam({ ...param, timeRange: state.timeRange, timeInterval: state.timeInterval, timeout: state.timeout });
|
||||||
}, [state.timeRange, state.timeInterval, state.timeout]);
|
}, [state.timeRange, state.timeInterval, state.timeout]);
|
||||||
|
|
||||||
const handleTimeChange = useCallback(({ start, end, timeInterval, timeout, refresh }) => {
|
const handleTimeChange = ({ start, end, timeInterval, timeout, refresh }) => {
|
||||||
setState(initState({
|
setState(initState({
|
||||||
...state,
|
...state,
|
||||||
|
param,
|
||||||
timeRange: {
|
timeRange: {
|
||||||
min: start,
|
min: start,
|
||||||
max: end,
|
max: end,
|
||||||
|
@ -130,11 +131,12 @@ const Monitor = (props) => {
|
||||||
timeout: timeout || state.timeout,
|
timeout: timeout || state.timeout,
|
||||||
refresh
|
refresh
|
||||||
}));
|
}));
|
||||||
}, [state])
|
}
|
||||||
|
|
||||||
const onInfoChange = (info) => {
|
const onInfoChange = (info) => {
|
||||||
setState({
|
setState({
|
||||||
...state,
|
...state,
|
||||||
|
param,
|
||||||
info,
|
info,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -156,7 +158,7 @@ const Monitor = (props) => {
|
||||||
return monitor_configs?.node_stats?.enabled === false && monitor_configs?.index_stats?.enabled === false
|
return monitor_configs?.node_stats?.enabled === false && monitor_configs?.index_stats?.enabled === false
|
||||||
}
|
}
|
||||||
return metric_collection_mode === 'agent'
|
return metric_collection_mode === 'agent'
|
||||||
}, [JSON.stringify(selectedCluster?.monitor_configs)])
|
}, [JSON.stringify(selectedCluster)])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -168,6 +170,7 @@ const Monitor = (props) => {
|
||||||
<>
|
<>
|
||||||
<div style={{ marginBottom: 5 }}>
|
<div style={{ marginBottom: 5 }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<div style={{ maxWidth: 600 }}>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
locale={getLocale()}
|
locale={getLocale()}
|
||||||
start={state.timeRange.min}
|
start={state.timeRange.min}
|
||||||
|
@ -180,7 +183,7 @@ const Monitor = (props) => {
|
||||||
onTimeSettingsChange(newRefresh)
|
onTimeSettingsChange(newRefresh)
|
||||||
setRefresh(newRefresh)
|
setRefresh(newRefresh)
|
||||||
}}
|
}}
|
||||||
onRefresh={handleTimeChange}
|
onRefresh={(value) => handleTimeChange({ ...(value || {}), refresh: new Date().valueOf()})}
|
||||||
showTimeSetting={true}
|
showTimeSetting={true}
|
||||||
showTimeInterval={true}
|
showTimeInterval={true}
|
||||||
timeInterval={state.timeInterval}
|
timeInterval={state.timeInterval}
|
||||||
|
@ -194,6 +197,7 @@ const Monitor = (props) => {
|
||||||
})
|
})
|
||||||
setState({
|
setState({
|
||||||
...state,
|
...state,
|
||||||
|
param,
|
||||||
timeInterval: timeSetting.timeInterval,
|
timeInterval: timeSetting.timeInterval,
|
||||||
timeout: timeSetting.timeout
|
timeout: timeSetting.timeout
|
||||||
});
|
});
|
||||||
|
@ -207,6 +211,7 @@ const Monitor = (props) => {
|
||||||
}}
|
}}
|
||||||
recentlyUsedRangesKey={'monitor'}
|
recentlyUsedRangesKey={'monitor'}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<CollectStatus fetchUrl={`${ESPrefix}/${selectedCluster?.id}/_collection_stats`}/>
|
<CollectStatus fetchUrl={`${ESPrefix}/${selectedCluster?.id}/_collection_stats`}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -221,7 +226,7 @@ const Monitor = (props) => {
|
||||||
animated={false}
|
animated={false}
|
||||||
>
|
>
|
||||||
{panes.map((pane) => (
|
{panes.map((pane) => (
|
||||||
<TabPane tab={pane.title} key={pane.key}>
|
<TabPane tab={formatMessage({id: `cluster.monitor.tabs.${pane.key}`})} key={pane.key}>
|
||||||
<Spin spinning={spinning && !!state.refresh}>
|
<Spin spinning={spinning && !!state.refresh}>
|
||||||
<StatisticBar
|
<StatisticBar
|
||||||
setSpinning={setSpinning}
|
setSpinning={setSpinning}
|
||||||
|
@ -249,6 +254,7 @@ const Monitor = (props) => {
|
||||||
})
|
})
|
||||||
setState({
|
setState({
|
||||||
...state,
|
...state,
|
||||||
|
param,
|
||||||
timeInterval,
|
timeInterval,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -121,6 +121,9 @@ function FilterBarUI(props: Props) {
|
||||||
onCancel={() => setIsAddFilterPopoverOpen(false)}
|
onCancel={() => setIsAddFilterPopoverOpen(false)}
|
||||||
key={JSON.stringify(newFilter)}
|
key={JSON.stringify(newFilter)}
|
||||||
services={props.services}
|
services={props.services}
|
||||||
|
dateRangeFrom={props.dateRangeFrom}
|
||||||
|
dateRangeTo={props.dateRangeTo}
|
||||||
|
timeField={props.timeField}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
|
|
|
@ -313,6 +313,9 @@ class FilterEditorUI extends Component<Props, State> {
|
||||||
onChange={this.onParamsChange}
|
onChange={this.onParamsChange}
|
||||||
data-test-subj="phraseValueInput"
|
data-test-subj="phraseValueInput"
|
||||||
services={this.props.services}
|
services={this.props.services}
|
||||||
|
dateRangeFrom={this.props.dateRangeFrom}
|
||||||
|
dateRangeTo={this.props.dateRangeTo}
|
||||||
|
timeField={this.props.timeField}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'phrases':
|
case 'phrases':
|
||||||
|
@ -323,6 +326,9 @@ class FilterEditorUI extends Component<Props, State> {
|
||||||
values={this.state.params}
|
values={this.state.params}
|
||||||
onChange={this.onParamsChange}
|
onChange={this.onParamsChange}
|
||||||
services={this.props.services}
|
services={this.props.services}
|
||||||
|
dateRangeFrom={this.props.dateRangeFrom}
|
||||||
|
dateRangeTo={this.props.dateRangeTo}
|
||||||
|
timeField={this.props.timeField}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'range':
|
case 'range':
|
||||||
|
|
|
@ -82,17 +82,30 @@ export class PhraseSuggestorUI<
|
||||||
protected updateSuggestions = debounce(async (query: string = "") => {
|
protected updateSuggestions = debounce(async (query: string = "") => {
|
||||||
if (this.abortController) this.abortController.abort();
|
if (this.abortController) this.abortController.abort();
|
||||||
this.abortController = new AbortController();
|
this.abortController = new AbortController();
|
||||||
const { indexPattern, field } = this.props as PhraseSuggestorProps;
|
const { indexPattern, field, dateRangeFrom, dateRangeTo, timeField } = this.props as PhraseSuggestorProps;
|
||||||
if (!field || !this.isSuggestingValues()) {
|
if (!field || !this.isSuggestingValues()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({ isLoading: true });
|
this.setState({ isLoading: true });
|
||||||
|
|
||||||
|
const boolFilter = []
|
||||||
|
if (dateRangeFrom && dateRangeTo && timeField) {
|
||||||
|
boolFilter.push({
|
||||||
|
"range": {
|
||||||
|
[timeField]: {
|
||||||
|
"gte": dateRangeFrom,
|
||||||
|
"lte": dateRangeTo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const suggestions = await this.props.services.data.autocomplete.getValueSuggestions(
|
const suggestions = await this.props.services.data.autocomplete.getValueSuggestions(
|
||||||
{
|
{
|
||||||
indexPattern,
|
indexPattern,
|
||||||
field,
|
field,
|
||||||
query,
|
query,
|
||||||
|
boolFilter,
|
||||||
signal: this.abortController.signal,
|
signal: this.abortController.signal,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -50,8 +50,7 @@ class PhraseValueInputUI extends PhraseSuggestorUI<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderWithSuggestions() {
|
private renderWithSuggestions() {
|
||||||
let { suggestions } = this.state;
|
const suggestions = Array.isArray(this.state.suggestions) ? this.state.suggestions : []
|
||||||
suggestions = suggestions || [];
|
|
||||||
const { value, intl, onChange } = this.props;
|
const { value, intl, onChange } = this.props;
|
||||||
// there are cases when the value is a number, this would cause an exception
|
// there are cases when the value is a number, this would cause an exception
|
||||||
const valueAsStr = String(value);
|
const valueAsStr = String(value);
|
||||||
|
|
|
@ -59,7 +59,7 @@
|
||||||
@include euiBreakpoint("m", "l", "xl") {
|
@include euiBreakpoint("m", "l", "xl") {
|
||||||
.kbnQueryBar__datePickerWrapper {
|
.kbnQueryBar__datePickerWrapper {
|
||||||
// sass-lint:disable-block no-important
|
// sass-lint:disable-block no-important
|
||||||
max-width: 340px;
|
max-width: 400px;
|
||||||
flex-grow: 0 !important;
|
flex-grow: 0 !important;
|
||||||
flex-basis: auto !important;
|
flex-basis: auto !important;
|
||||||
margin-right: -$euiSizeXS;
|
margin-right: -$euiSizeXS;
|
||||||
|
|
|
@ -264,7 +264,7 @@ export default function QueryBarTopRow(props: QueryBarTopRowProps) {
|
||||||
return (
|
return (
|
||||||
<NoDataPopover storage={storage} showNoDataPopover={props.indicateNoData}>
|
<NoDataPopover storage={storage} showNoDataPopover={props.indicateNoData}>
|
||||||
<EuiFlexGroup responsive={false} gutterSize="s">
|
<EuiFlexGroup responsive={false} gutterSize="s">
|
||||||
{renderHistogram()}
|
{/* {renderHistogram()} */}
|
||||||
{renderDatePicker()}
|
{renderDatePicker()}
|
||||||
<EuiFlexItem grow={false}>{button}</EuiFlexItem>
|
<EuiFlexItem grow={false}>{button}</EuiFlexItem>
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
|
|
|
@ -484,6 +484,9 @@ class SearchBarUI extends Component<SearchBarProps, State> {
|
||||||
filters={this.props.filters!}
|
filters={this.props.filters!}
|
||||||
onFiltersUpdated={this.props.onFiltersUpdated}
|
onFiltersUpdated={this.props.onFiltersUpdated}
|
||||||
indexPatterns={this.props.indexPatterns!}
|
indexPatterns={this.props.indexPatterns!}
|
||||||
|
dateRangeFrom={this.state.dateRangeFrom}
|
||||||
|
dateRangeTo={this.state.dateRangeTo}
|
||||||
|
timeField={this.props.timeSetting?.timeField}
|
||||||
services={this.props.services}
|
services={this.props.services}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -88,7 +88,7 @@ export class DiscoverHistogram extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const timeZone = getTimezone();
|
const timeZone = getTimezone();
|
||||||
const { chartData } = this.props;
|
const { chartData, height = 100 } = this.props;
|
||||||
|
|
||||||
const { chartsTheme, chartsBaseTheme } = this.state;
|
const { chartsTheme, chartsBaseTheme } = this.state;
|
||||||
|
|
||||||
|
@ -149,7 +149,7 @@ export class DiscoverHistogram extends Component {
|
||||||
//console.log(data)
|
//console.log(data)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Chart size={{ height: 40 }}>
|
<Chart size={{ height }}>
|
||||||
<Settings
|
<Settings
|
||||||
xDomain={xDomain}
|
xDomain={xDomain}
|
||||||
onBrushEnd={this.onBrushEnd}
|
onBrushEnd={this.onBrushEnd}
|
||||||
|
|
|
@ -121,6 +121,7 @@ export default {
|
||||||
"form.button.restart": "Restart",
|
"form.button.restart": "Restart",
|
||||||
"form.button.verify": "Verify",
|
"form.button.verify": "Verify",
|
||||||
"form.button.clean": "Clean",
|
"form.button.clean": "Clean",
|
||||||
|
"form.button.view_logs": "View Logs",
|
||||||
"form.button.clean.confim.desc": "Are you sure to clean data that is {status}?",
|
"form.button.clean.confim.desc": "Are you sure to clean data that is {status}?",
|
||||||
"form.button.clean.unavailable.nodes": "Clean unavailable nodes",
|
"form.button.clean.unavailable.nodes": "Clean unavailable nodes",
|
||||||
"form.button.clean.unavailable.nodes.desc": "Are you sure to clean nodes that are unavailable within seven days?",
|
"form.button.clean.unavailable.nodes.desc": "Are you sure to clean nodes that are unavailable within seven days?",
|
||||||
|
|
|
@ -43,4 +43,7 @@ export default {
|
||||||
|
|
||||||
"agent.label.agent_credential": "Agent Credential",
|
"agent.label.agent_credential": "Agent Credential",
|
||||||
"agent.credential.tip": "No credential required",
|
"agent.credential.tip": "No credential required",
|
||||||
|
"agent.instance.clear.title": "Clear Offline Instances",
|
||||||
|
"agent.instance.clear.modal.title": "Are you sure you want to clear offline instances?",
|
||||||
|
"agent.instance.clear.modal.desc": "This operation will delete offline instances that have not reported metrics for 7 days."
|
||||||
};
|
};
|
||||||
|
|
|
@ -124,6 +124,23 @@ export default {
|
||||||
"cluster.monitor.topn.color": "Color Metric",
|
"cluster.monitor.topn.color": "Color Metric",
|
||||||
"cluster.monitor.topn.theme": "Theme",
|
"cluster.monitor.topn.theme": "Theme",
|
||||||
|
|
||||||
|
"cluster.monitor.logs.timestamp": "Timestamp",
|
||||||
|
"cluster.monitor.logs.type": "Type",
|
||||||
|
"cluster.monitor.logs.level": "Level",
|
||||||
|
"cluster.monitor.logs.node": "Node",
|
||||||
|
"cluster.monitor.logs.message": "Message",
|
||||||
|
"cluster.monitor.logs.search.placeholder": "Search message",
|
||||||
|
"cluster.monitor.logs.empty.agent": "No data, please change the time range or check if the Agent is working properly.",
|
||||||
|
"cluster.monitor.logs.empty.agentless": "No data, please install the Agent and change the cluster collection mode to Agent.",
|
||||||
|
|
||||||
|
"cluster.monitor.tabs.overview": "Overview",
|
||||||
|
"cluster.monitor.tabs.advanced": "Advanced",
|
||||||
|
"cluster.monitor.tabs.topn": "TopN",
|
||||||
|
"cluster.monitor.tabs.logs": "Logs",
|
||||||
|
"cluster.monitor.tabs.nodes": "Nodes",
|
||||||
|
"cluster.monitor.tabs.indices": "Indices",
|
||||||
|
"cluster.monitor.tabs.shards": "Shards",
|
||||||
|
|
||||||
"cluster.metrics.axis.index_throughput.title": "Indexing Rate",
|
"cluster.metrics.axis.index_throughput.title": "Indexing Rate",
|
||||||
"cluster.metrics.axis.search_throughput.title": "Search Rate",
|
"cluster.metrics.axis.search_throughput.title": "Search Rate",
|
||||||
"cluster.metrics.axis.index_latency.title": "Indexing Latency",
|
"cluster.metrics.axis.index_latency.title": "Indexing Latency",
|
||||||
|
@ -371,4 +388,6 @@ export default {
|
||||||
|
|
||||||
"cluster.collect.last_active_at": "Last Active At",
|
"cluster.collect.last_active_at": "Last Active At",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -126,6 +126,7 @@ export default {
|
||||||
"form.button.restart": "重启",
|
"form.button.restart": "重启",
|
||||||
"form.button.verify": "校验",
|
"form.button.verify": "校验",
|
||||||
"form.button.clean": "清除",
|
"form.button.clean": "清除",
|
||||||
|
"form.button.view_logs": "View Logs",
|
||||||
"form.button.clean.confim.desc": "确定删除状态为 {status} 的数据吗?",
|
"form.button.clean.confim.desc": "确定删除状态为 {status} 的数据吗?",
|
||||||
"form.button.clean.unavailable.nodes": "清除不可用节点",
|
"form.button.clean.unavailable.nodes": "清除不可用节点",
|
||||||
"form.button.clean.unavailable.nodes.desc": "确定清除7天内不可用的节点吗?",
|
"form.button.clean.unavailable.nodes.desc": "确定清除7天内不可用的节点吗?",
|
||||||
|
|
|
@ -40,4 +40,7 @@ export default {
|
||||||
|
|
||||||
"agent.label.agent_credential": "代理凭据",
|
"agent.label.agent_credential": "代理凭据",
|
||||||
"agent.credential.tip": "不需要凭据",
|
"agent.credential.tip": "不需要凭据",
|
||||||
|
"agent.instance.clear.title": "清理离线实例",
|
||||||
|
"agent.instance.clear.modal.title": "您确定要清理离线实例?",
|
||||||
|
"agent.instance.clear.modal.desc": "该操作将会删除离线并且 7 天没有上报指标的实例"
|
||||||
};
|
};
|
||||||
|
|
|
@ -115,6 +115,23 @@ export default {
|
||||||
"cluster.monitor.topn.color": "颜色指标",
|
"cluster.monitor.topn.color": "颜色指标",
|
||||||
"cluster.monitor.topn.theme": "主题",
|
"cluster.monitor.topn.theme": "主题",
|
||||||
|
|
||||||
|
"cluster.monitor.logs.timestamp": "时间戳",
|
||||||
|
"cluster.monitor.logs.type": "类型",
|
||||||
|
"cluster.monitor.logs.level": "等级",
|
||||||
|
"cluster.monitor.logs.node": "节点",
|
||||||
|
"cluster.monitor.logs.message": "日志",
|
||||||
|
"cluster.monitor.logs.search.placeholder": "搜索日志",
|
||||||
|
"cluster.monitor.logs.empty.agent": "没有数据,请更改时间范围或检查 Agent 是否正常工作。",
|
||||||
|
"cluster.monitor.logs.empty.agentless": "没有数据,请安装 Agent 并更改集群采集模式为 Agent 。",
|
||||||
|
|
||||||
|
"cluster.monitor.tabs.overview": "概览",
|
||||||
|
"cluster.monitor.tabs.advanced": "高级",
|
||||||
|
"cluster.monitor.tabs.topn": "TopN",
|
||||||
|
"cluster.monitor.tabs.logs": "日志",
|
||||||
|
"cluster.monitor.tabs.nodes": "节点",
|
||||||
|
"cluster.monitor.tabs.indices": "索引",
|
||||||
|
"cluster.monitor.tabs.shards": "分片",
|
||||||
|
|
||||||
"cluster.metrics.axis.index_throughput.title": "索引吞吐",
|
"cluster.metrics.axis.index_throughput.title": "索引吞吐",
|
||||||
"cluster.metrics.axis.search_throughput.title": "查询吞吐",
|
"cluster.metrics.axis.search_throughput.title": "查询吞吐",
|
||||||
"cluster.metrics.axis.index_latency.title": "索引延迟",
|
"cluster.metrics.axis.index_latency.title": "索引延迟",
|
||||||
|
|
|
@ -355,7 +355,10 @@ export default {
|
||||||
let idx = state.clusterList.findIndex((item) => item.id === payload.id);
|
let idx = state.clusterList.findIndex((item) => item.id === payload.id);
|
||||||
idx > -1 && (state.clusterList[idx].name = payload.name);
|
idx > -1 && (state.clusterList[idx].name = payload.name);
|
||||||
if (state.selectedCluster?.id === payload.id) {
|
if (state.selectedCluster?.id === payload.id) {
|
||||||
state.selectedCluster.monitor_configs = payload.monitor_configs
|
state.selectedCluster = {
|
||||||
|
...(state.selectedCluster || {}),
|
||||||
|
...(payload || {})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
state.clusterStatus[payload.id].config.monitored = payload.monitored;
|
state.clusterStatus[payload.id].config.monitored = payload.monitored;
|
||||||
return state;
|
return state;
|
||||||
|
|
|
@ -379,6 +379,37 @@ const AgentList = (props) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [clearLoading, setClearLoading] = useState(false)
|
||||||
|
const onClearClick = async ()=>{
|
||||||
|
setClearLoading(true);
|
||||||
|
const statusRes = await request(`/instance/_clear`, {
|
||||||
|
method: "POST",
|
||||||
|
queryParams: {
|
||||||
|
"app_name": "agent",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if(statusRes && statusRes.acknowledged){
|
||||||
|
message.success("submit successfully");
|
||||||
|
}
|
||||||
|
setClearLoading(false);
|
||||||
|
}
|
||||||
|
const showClearConfirm = useCallback(() => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: formatMessage({ id: "agent.instance.clear.modal.title" }),
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
<div>{formatMessage({ id: "agent.instance.clear.modal.desc" })}</div>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
okText: "Yes",
|
||||||
|
okType: "danger",
|
||||||
|
cancelText: "No",
|
||||||
|
onOk() {
|
||||||
|
onClearClick();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageHeaderWrapper>
|
<PageHeaderWrapper>
|
||||||
<Card>
|
<Card>
|
||||||
|
@ -390,7 +421,7 @@ const AgentList = (props) => {
|
||||||
marginBottom: 15,
|
marginBottom: 15,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ maxWidth: 500, flex: "1 1 auto" }}>
|
<div style={{ maxWidth: 450, flex: "1 1 auto" }}>
|
||||||
<Search
|
<Search
|
||||||
allowClear
|
allowClear
|
||||||
placeholder="Type keyword to search"
|
placeholder="Type keyword to search"
|
||||||
|
@ -413,6 +444,9 @@ const AgentList = (props) => {
|
||||||
{
|
{
|
||||||
hasAuthority("agent.instance:all") && (
|
hasAuthority("agent.instance:all") && (
|
||||||
<>
|
<>
|
||||||
|
<Button loading={clearLoading} onClick={showClearConfirm}>
|
||||||
|
{formatMessage({ id: "agent.instance.clear.title" })}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
|
@ -666,7 +666,7 @@ const Index = (props) => {
|
||||||
onRangeChange={onTimeChange}
|
onRangeChange={onTimeChange}
|
||||||
{...refresh}
|
{...refresh}
|
||||||
onRefreshChange={setRefresh}
|
onRefreshChange={setRefresh}
|
||||||
onRefresh={({ start, end}) => onTimeChange({ start, end, refresh: new Date().valueOf()})}
|
onRefresh={(value) => onTimeChange({ ...(value || {}), refresh: new Date().valueOf()})}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
onTimeZoneChange={setTimeZone}
|
onTimeZoneChange={setTimeZone}
|
||||||
recentlyUsedRangesKey={'alerting-message'}
|
recentlyUsedRangesKey={'alerting-message'}
|
||||||
|
@ -682,14 +682,6 @@ const Index = (props) => {
|
||||||
gap: 10,
|
gap: 10,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
|
||||||
icon="redo"
|
|
||||||
onClick={() => {
|
|
||||||
onRefreshClick();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{formatMessage({ id: "form.button.refresh" })}
|
|
||||||
</Button>
|
|
||||||
{hasAuthority("alerting.message:all") ? (
|
{hasAuthority("alerting.message:all") ? (
|
||||||
<Dropdown overlay={batchMenu}>
|
<Dropdown overlay={batchMenu}>
|
||||||
<Button type="primary">
|
<Button type="primary">
|
||||||
|
|
|
@ -123,18 +123,6 @@ const MessageDetail = (props) => {
|
||||||
recentlyUsedRangesKey={"rule-detail"}
|
recentlyUsedRangesKey={"rule-detail"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
handleTimeChange({
|
|
||||||
start: timeRange.min,
|
|
||||||
end: timeRange.max,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
icon={"reload"}
|
|
||||||
type="primary"
|
|
||||||
>
|
|
||||||
{formatMessage({ id: "form.button.refresh" })}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<div style={{marginTop: 15,display:"flex", gap: 15, marginBottom:10}}>
|
<div style={{marginTop: 15,display:"flex", gap: 15, marginBottom:10}}>
|
||||||
<div style={{flex: "1 1 50%"}}>
|
<div style={{flex: "1 1 50%"}}>
|
||||||
|
|
|
@ -333,7 +333,7 @@ const RuleDetail = (props) => {
|
||||||
onRangeChange={handleTimeChange}
|
onRangeChange={handleTimeChange}
|
||||||
{...refresh}
|
{...refresh}
|
||||||
onRefreshChange={setRefresh}
|
onRefreshChange={setRefresh}
|
||||||
onRefresh={({ start, end}) => handleTimeChange({ start, end, refresh: new Date().valueOf()})}
|
onRefresh={(value) => handleTimeChange({ ...(value || {}), refresh: new Date().valueOf()})}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
onTimeZoneChange={setTimeZone}
|
onTimeZoneChange={setTimeZone}
|
||||||
recentlyUsedRangesKey={"rule-detail"}
|
recentlyUsedRangesKey={"rule-detail"}
|
||||||
|
|
|
@ -178,6 +178,9 @@ const Discover = (props) => {
|
||||||
field: "",
|
field: "",
|
||||||
enabled: false,
|
enabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [histogramVisible, setHistogramVisible] = useState(false)
|
||||||
|
|
||||||
const [distinctParams, setDistinctParams] = React.useState(
|
const [distinctParams, setDistinctParams] = React.useState(
|
||||||
distinctParamsDefault
|
distinctParamsDefault
|
||||||
);
|
);
|
||||||
|
@ -1122,7 +1125,7 @@ const Discover = (props) => {
|
||||||
getVisualizations={() => visRef?.current?.getVisualizations()}
|
getVisualizations={() => visRef?.current?.getVisualizations()}
|
||||||
searchInfo={{
|
searchInfo={{
|
||||||
took,
|
took,
|
||||||
hits,
|
total: hits,
|
||||||
...timeChartProps,
|
...timeChartProps,
|
||||||
}}
|
}}
|
||||||
selectedQueriesId={selectedQueriesId}
|
selectedQueriesId={selectedQueriesId}
|
||||||
|
@ -1160,6 +1163,12 @@ const Discover = (props) => {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
showLayoutListIcon={false}
|
showLayoutListIcon={false}
|
||||||
|
histogramProps={{
|
||||||
|
histogramData,
|
||||||
|
onHistogramToggle: () => {
|
||||||
|
setHistogramVisible(!histogramVisible)
|
||||||
|
},
|
||||||
|
}}
|
||||||
// viewLayout={viewLayout}
|
// viewLayout={viewLayout}
|
||||||
// onViewLayoutChange={(layout) => {
|
// onViewLayoutChange={(layout) => {
|
||||||
// if (layout) {
|
// if (layout) {
|
||||||
|
@ -1306,6 +1315,31 @@ const Discover = (props) => {
|
||||||
responsive={false}
|
responsive={false}
|
||||||
style={{ position: "relative" }}
|
style={{ position: "relative" }}
|
||||||
>
|
>
|
||||||
|
{histogramVisible && opts.timefield && (
|
||||||
|
<EuiFlexItem>
|
||||||
|
<section
|
||||||
|
aria-label={"Histogram of found documents"}
|
||||||
|
className="dscTimechart"
|
||||||
|
>
|
||||||
|
{opts.chartAggConfigs &&
|
||||||
|
histogramData &&
|
||||||
|
records.length !== 0 && (
|
||||||
|
<div
|
||||||
|
className="dscHistogramGrid"
|
||||||
|
data-test-subj="discoverChart"
|
||||||
|
>
|
||||||
|
<DiscoverHistogram
|
||||||
|
chartData={histogramData}
|
||||||
|
timefilterUpdateHandler={
|
||||||
|
timefilterUpdateHandler
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
<EuiSpacer size="s" />
|
||||||
|
</EuiFlexItem>
|
||||||
|
)}
|
||||||
<EuiFlexItem className="eui-yScroll">
|
<EuiFlexItem className="eui-yScroll">
|
||||||
<section
|
<section
|
||||||
className="dscTable eui-yScroll"
|
className="dscTable eui-yScroll"
|
||||||
|
|
|
@ -744,6 +744,7 @@ class Index extends PureComponent {
|
||||||
onChangeDeleteIndexConfirmState={this.onChangeDeleteIndexConfirmState}
|
onChangeDeleteIndexConfirmState={this.onChangeDeleteIndexConfirmState}
|
||||||
deleteIndexConfirm={this.state.deleteIndexConfirm}
|
deleteIndexConfirm={this.state.deleteIndexConfirm}
|
||||||
items={this.state.deleteIndexItems}
|
items={this.state.deleteIndexItems}
|
||||||
|
selectedCluster={this.props.selectedCluster}
|
||||||
/>
|
/>
|
||||||
</PageHeaderWrapper>
|
</PageHeaderWrapper>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { DiscoverHistogram } from "@/components/vendor/discover/public/application/components/histogram/histogram";
|
||||||
|
import { Icon, Popover } from "antd"
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import styles from "./index.less";
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
|
||||||
|
const { onHistogramToggle } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Icon type="bar-chart" title="show histogram" style={{color: '#006BB4', cursor: 'pointer'}} onClick={() => {
|
||||||
|
onHistogramToggle()
|
||||||
|
}}/>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
.histogram {
|
||||||
|
z-index: 1;
|
||||||
|
:global {
|
||||||
|
.ant-popover-inner-content {
|
||||||
|
width: 400px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,9 +5,9 @@ import InsightConfig, { ISearchConfig } from "../InsightConfig";
|
||||||
import styles from './index.less';
|
import styles from './index.less';
|
||||||
import { create, list, remove, update } from "../services/queries";
|
import { create, list, remove, update } from "../services/queries";
|
||||||
import FullScreen from "../FullScreen";
|
import FullScreen from "../FullScreen";
|
||||||
import ModeHandler from "../ModeHandler";
|
|
||||||
import { Icon, message } from "antd";
|
import { Icon, message } from "antd";
|
||||||
import SearchInfo from "../SearchInfo";
|
import SearchInfo from "../SearchInfo";
|
||||||
|
import Histogram from "../Histogram";
|
||||||
import ViewLayout from "../ViewLayout";
|
import ViewLayout from "../ViewLayout";
|
||||||
|
|
||||||
export interface IQueries {
|
export interface IQueries {
|
||||||
|
@ -72,7 +72,8 @@ export default forwardRef((props: IProps, ref: any) => {
|
||||||
onSearchConfigChange,
|
onSearchConfigChange,
|
||||||
showLayoutListIcon,
|
showLayoutListIcon,
|
||||||
viewLayout,
|
viewLayout,
|
||||||
onViewLayoutChange
|
onViewLayoutChange,
|
||||||
|
histogramProps = {}
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -183,6 +184,7 @@ export default forwardRef((props: IProps, ref: any) => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.bar}>
|
<div className={styles.bar}>
|
||||||
<SearchInfo {...searchInfo} loading={searchLoading}/>
|
<SearchInfo {...searchInfo} loading={searchLoading}/>
|
||||||
|
{ histogramProps?.histogramData && <Histogram {...histogramProps}/>}
|
||||||
<SaveQueries
|
<SaveQueries
|
||||||
tags={tags}
|
tags={tags}
|
||||||
onTagsChange={setTags}
|
onTagsChange={setTags}
|
||||||
|
|
|
@ -30,7 +30,7 @@ export interface IProps {
|
||||||
* selected interval
|
* selected interval
|
||||||
*/
|
*/
|
||||||
stateInterval: string;
|
stateInterval: string;
|
||||||
hits: number;
|
total: number;
|
||||||
took?: number;
|
took?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ export default ({
|
||||||
dateFormat,
|
dateFormat,
|
||||||
timeRange,
|
timeRange,
|
||||||
stateInterval,
|
stateInterval,
|
||||||
hits,
|
total,
|
||||||
took,
|
took,
|
||||||
}: IProps) => {
|
}: IProps) => {
|
||||||
const [interval, setInterval] = useState(stateInterval);
|
const [interval, setInterval] = useState(stateInterval);
|
||||||
|
@ -69,7 +69,7 @@ export default ({
|
||||||
>
|
>
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<div style={{ fontSize: 12}}>
|
<div style={{ fontSize: 12}}>
|
||||||
Found <span style={{fontWeight: "bold" }}>{hits}</span>{" "}
|
Found <span style={{fontWeight: "bold" }}>{total}</span>{" "}
|
||||||
records {took && (
|
records {took && (
|
||||||
<span style={{marginLeft: 5 }}>
|
<span style={{marginLeft: 5 }}>
|
||||||
({took} milliscond)
|
({took} milliscond)
|
||||||
|
|
|
@ -1,17 +1,40 @@
|
||||||
import { Icon, Popover } from "antd"
|
import { Icon, Popover } from "antd"
|
||||||
import { useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import Info, { IProps } from "./Info";
|
import Info, { IProps } from "./Info";
|
||||||
import styles from './index.scss';
|
import styles from './index.scss';
|
||||||
|
|
||||||
export default (props: IProps & { loading: boolean }) => {
|
export default (props: IProps & { loading: boolean }) => {
|
||||||
|
|
||||||
const [showResultCount, setShowResultCount] = useState(true);
|
const { loading, total } = props
|
||||||
|
|
||||||
if (typeof props.hits !== 'number' || props.hits <= 0) return null;
|
const [showResultCount, setShowResultCount] = useState(true);
|
||||||
|
const timerRef = useRef(null)
|
||||||
|
const autoHiddenRef = useRef(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearTimeout(timerRef.current)
|
||||||
|
}
|
||||||
|
if (showResultCount) {
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
if (autoHiddenRef.current) {
|
||||||
|
setShowResultCount(false)
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}, [showResultCount])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loading) {
|
||||||
|
autoHiddenRef.current = true
|
||||||
|
}
|
||||||
|
}, [loading])
|
||||||
|
|
||||||
|
if (typeof total !== 'number' || total <= 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
visible={!props.loading && showResultCount}
|
visible={!loading && showResultCount}
|
||||||
placement="left"
|
placement="left"
|
||||||
title={null}
|
title={null}
|
||||||
overlayClassName={styles.searchInfo}
|
overlayClassName={styles.searchInfo}
|
||||||
|
@ -21,7 +44,14 @@ export default (props: IProps & { loading: boolean }) => {
|
||||||
dateFormat={"YYYY-MM-DD H:mm"}
|
dateFormat={"YYYY-MM-DD H:mm"}
|
||||||
/>
|
/>
|
||||||
)}>
|
)}>
|
||||||
<Icon type="info-circle" style={{color: '#006BB4', cursor: 'pointer'}} onClick={() => setShowResultCount(!showResultCount)}/>
|
<Icon type="info-circle" style={{color: '#006BB4', cursor: 'pointer'}} onClick={() => {
|
||||||
|
if (showResultCount) {
|
||||||
|
autoHiddenRef.current = true
|
||||||
|
} else {
|
||||||
|
autoHiddenRef.current = false
|
||||||
|
}
|
||||||
|
setShowResultCount(!showResultCount)
|
||||||
|
}}/>
|
||||||
</Popover>
|
</Popover>
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -41,7 +41,7 @@ export default (props) => {
|
||||||
to = bounds.max;
|
to = bounds.max;
|
||||||
}
|
}
|
||||||
if (!from || !to) return data
|
if (!from || !to) return data
|
||||||
const newData = cloneDeep(data)
|
const newData = cloneDeep(data.sort((a, b) => a.timestamp - b.timestamp))
|
||||||
const fromTimestamp = moment(from).valueOf();
|
const fromTimestamp = moment(from).valueOf();
|
||||||
const toTimestamp = moment(to).valueOf();
|
const toTimestamp = moment(to).valueOf();
|
||||||
let start = newData[0].timestamp;
|
let start = newData[0].timestamp;
|
||||||
|
|
|
@ -14,7 +14,7 @@ export default (props) => {
|
||||||
|
|
||||||
const { record, result, options, isGroup, isLock, onReady, bucketSize, isTimeSeries, highlightRange, currentQueries = {}, handleContextMenu, onChartElementClick } = props;
|
const { record, result, options, isGroup, isLock, onReady, bucketSize, isTimeSeries, highlightRange, currentQueries = {}, handleContextMenu, onChartElementClick } = props;
|
||||||
|
|
||||||
const { id, is_stack, is_percent, drilling = {}, series, legend } = record;
|
const { id, is_stack, is_percent, drilling = {}, series, legend, colors } = record;
|
||||||
|
|
||||||
const { metric = {} } = series[0]
|
const { metric = {} } = series[0]
|
||||||
|
|
||||||
|
@ -84,6 +84,13 @@ export default (props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (colors) {
|
||||||
|
config.color = Array.isArray(colors) ? colors : (value) => {
|
||||||
|
const { name } = value;
|
||||||
|
return colors[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const dataDrillingMenuItem = {
|
const dataDrillingMenuItem = {
|
||||||
type: TYPE_DATA_DRILLING,
|
type: TYPE_DATA_DRILLING,
|
||||||
name: formatMessage({id: "dashboard.widget.sub.menu.data.drilling"}),
|
name: formatMessage({id: "dashboard.widget.sub.menu.data.drilling"}),
|
||||||
|
|
|
@ -84,6 +84,7 @@ export const WidgetRender = (props) => {
|
||||||
onResultChange={(res) => {
|
onResultChange={(res) => {
|
||||||
setRequests(Array.isArray(res) ? res.filter((item) => !!item.request).map((item) => item.request) : [])
|
setRequests(Array.isArray(res) ? res.filter((item) => !!item.request).map((item) => item.request) : [])
|
||||||
}}
|
}}
|
||||||
|
refresh={refresh}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
) : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: none;
|
display: none;
|
||||||
right: 0px;
|
right: 0px;
|
||||||
top: 0px;
|
bottom: 0px;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Modal, Checkbox, Tag, Badge, Alert, Icon } from "antd";
|
import { Modal, Checkbox, Tag, Badge, Alert, Icon, Tooltip } from "antd";
|
||||||
import { useCallback, useState, forwardRef, useMemo } from "react";
|
import { useCallback, useState, forwardRef, useMemo } from "react";
|
||||||
import useFetch from "@/lib/hooks/use_fetch";
|
import useFetch from "@/lib/hooks/use_fetch";
|
||||||
import request from "@/utils/request";
|
import request from "@/utils/request";
|
||||||
|
@ -28,7 +28,7 @@ export default (props) => {
|
||||||
onOk={props.onOk}
|
onOk={props.onOk}
|
||||||
okButtonProps={{ disabled: !props.deleteIndexConfirm }}
|
okButtonProps={{ disabled: !props.deleteIndexConfirm }}
|
||||||
>
|
>
|
||||||
<p>You are about to delete these indices:</p>
|
<p>You are about to delete these indices in cluster <Tooltip title={props.selectedCluster.id}><b>{props.selectedCluster.name}</b></Tooltip>:</p>
|
||||||
<ul style={{ maxHeight: 240, overflow: "scroll" }}>
|
<ul style={{ maxHeight: 240, overflow: "scroll" }}>
|
||||||
{props.items.map((item) => {
|
{props.items.map((item) => {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -13,14 +13,19 @@ export default ({ formData }) => {
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
isInit,
|
isInit,
|
||||||
|
hosts = [],
|
||||||
bootstrap_username,
|
bootstrap_username,
|
||||||
bootstrap_password,
|
bootstrap_password,
|
||||||
credential_secret,
|
credential_secret,
|
||||||
} = formData;
|
} = formData;
|
||||||
|
|
||||||
const onDownload = () => {
|
const onDownload = () => {
|
||||||
|
let hostV = host
|
||||||
|
if (!hostV && hosts.length > 0) {
|
||||||
|
hostV = hosts[0]
|
||||||
|
}
|
||||||
const data = {
|
const data = {
|
||||||
"Cluster": isTLS ? `https://${host}` : `http://${host}`,
|
"Cluster": isTLS ? `https://${hostV}` : `http://${hostV}`,
|
||||||
"Cluster Username": username,
|
"Cluster Username": username,
|
||||||
"Cluster Password": password,
|
"Cluster Password": password,
|
||||||
"Cluster Username": username,
|
"Cluster Username": username,
|
||||||
|
|
|
@ -24,6 +24,10 @@ export default (props) => {
|
||||||
if (indexName && indexName.includes("%")) {
|
if (indexName && indexName.includes("%")) {
|
||||||
indexNameEncode = encodeURIComponent(indexName);
|
indexNameEncode = encodeURIComponent(indexName);
|
||||||
}
|
}
|
||||||
|
const logStartTime = moment(timestamp).add(-3, "m");
|
||||||
|
const logEndTime = moment(timestamp).add(3, "m");
|
||||||
|
const logTimeRangeStr = encodeURIComponent(JSON.stringify({min:logStartTime.toISOString(),max:logEndTime.toISOString()}))
|
||||||
|
|
||||||
|
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case "index_state_change":
|
case "index_state_change":
|
||||||
|
@ -90,10 +94,20 @@ export default (props) => {
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
<b>{opers[type]}</b> from <b>{hit._source.metadata.labels.from}</b> to{" "}
|
<b>{opers[type]}</b> from <b>{hit._source.metadata.labels.from}</b> to{" "}
|
||||||
<b>{hit._source.metadata.labels.to}</b>
|
<b>{hit._source.metadata.labels.to}</b>
|
||||||
|
<a
|
||||||
|
size="small"
|
||||||
|
style={{ marginLeft: 12, cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
<Icon type="file-text" style={{marginRight:2}}/>
|
||||||
|
<Link
|
||||||
|
to={`/cluster/monitor/elasticsearch/${hit._source.metadata.labels.cluster_id}?_g={"tab":"logs","timeRange":${logTimeRangeStr}}`}
|
||||||
|
>{formatMessage({ id: "form.button.view_logs" })}
|
||||||
|
</Link>
|
||||||
|
</a>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
case "cluster_health_change":
|
case "cluster_health_change":
|
||||||
return <ClusterHealthChange hit={hit} type={opers[type]} timeRangeStr={timeRangeStr}/>
|
return <ClusterHealthChange hit={hit} type={opers[type]} timeRangeStr={timeRangeStr} logTimeRangeStr={logTimeRangeStr}/>
|
||||||
case "node_health_change":
|
case "node_health_change":
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -110,6 +124,15 @@ export default (props) => {
|
||||||
{hit._source.metadata.labels.cluster_name}
|
{hit._source.metadata.labels.cluster_name}
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
<b>{opers[type]}</b> to <b>{hit._source.metadata.labels.to}</b>
|
<b>{opers[type]}</b> to <b>{hit._source.metadata.labels.to}</b>
|
||||||
|
<a
|
||||||
|
size="small"
|
||||||
|
style={{ marginLeft: 12, cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
<Icon type="file-text" style={{marginRight:2}}/>
|
||||||
|
<Link
|
||||||
|
to={`/cluster/monitor/${hit._source.metadata.labels.cluster_id}/nodes/${hit._source.metadata.labels.node_id}?_g={"tab":"logs","timeRange":${logTimeRangeStr}}`}
|
||||||
|
>{formatMessage({ id: "form.button.view_logs" })}</Link>
|
||||||
|
</a>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
case "node_state_change":
|
case "node_state_change":
|
||||||
|
@ -211,17 +234,30 @@ export default (props) => {
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
case "metric_collection_mode_change":
|
||||||
|
if (type == "update") {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
metric collection mode of cluster{" "}
|
||||||
|
<Link
|
||||||
|
to={`/resource/cluster/${hit._source.metadata.labels.cluster_id}/edit`}
|
||||||
|
>
|
||||||
|
{hit._source.metadata.labels.cluster_name}
|
||||||
|
</Link>{" "}
|
||||||
|
was <b>changed from {hit._source.metadata.labels.from} to {hit._source.metadata.labels.to}</b>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return <></>;
|
return <></>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ClusterHealthChange = (props) => {
|
const ClusterHealthChange = (props) => {
|
||||||
const { hit, type, timeRangeStr } = props;
|
const { hit, type, timeRangeStr, logTimeRangeStr } = props;
|
||||||
const status = hit._source.metadata.labels.to
|
const status = hit._source.metadata.labels.to
|
||||||
const hasAllocationExplain = status === 'red'
|
const hasAllocationExplain = status === 'red'
|
||||||
|
|
||||||
const [active, setActive] = useState(false)
|
const [active, setActive] = useState(false)
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<span
|
<span
|
||||||
style={{ cursor: hasAllocationExplain ? 'pointer' : 'default'}}
|
style={{ cursor: hasAllocationExplain ? 'pointer' : 'default'}}
|
||||||
|
@ -251,6 +287,15 @@ const ClusterHealthChange = (props) => {
|
||||||
<Icon type={active ? "up-square" : "down-square"}/> {formatMessage({ id: "form.button.detail" })}
|
<Icon type={active ? "up-square" : "down-square"}/> {formatMessage({ id: "form.button.detail" })}
|
||||||
</a>
|
</a>
|
||||||
) : null}
|
) : null}
|
||||||
|
<a
|
||||||
|
size="small"
|
||||||
|
style={{ marginLeft: 12, cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
<Icon type="file-text" style={{marginRight:2}}/>
|
||||||
|
<Link
|
||||||
|
to={`/cluster/monitor/elasticsearch/${hit._source.metadata.labels.cluster_id}?_g={"tab":"logs","timeRange":${logTimeRangeStr}}`}
|
||||||
|
>{formatMessage({ id: "form.button.view_logs" })}</Link>
|
||||||
|
</a>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { Empty, Input, Spin, Table } from "antd";
|
||||||
|
import { formatMessage } from "umi/locale";
|
||||||
|
import { Link } from "umi";
|
||||||
|
import Logs from "../../components/Logs";
|
||||||
|
|
||||||
|
const AGGS = {
|
||||||
|
"Node":{"terms":{"field":"metadata.labels.node_name", "size": 1000 }},
|
||||||
|
"Type":{"terms":{"field":"metadata.name", "size": 1000 }},
|
||||||
|
"Level":{"terms":{"field":"payload.level", "size": 1000 }},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
const { clusterID, param = {} } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Logs
|
||||||
|
{...props}
|
||||||
|
aggs={AGGS}
|
||||||
|
queryFilters={[
|
||||||
|
{
|
||||||
|
"term": {
|
||||||
|
"metadata.labels.cluster_id": clusterID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
extraColumns={[
|
||||||
|
{
|
||||||
|
title: formatMessage({ id: "cluster.monitor.logs.node" }),
|
||||||
|
key: "metadata.labels.node_name",
|
||||||
|
dataIndex: "metadata.labels.node_name",
|
||||||
|
width: 88,
|
||||||
|
ellipsis: true,
|
||||||
|
render: (value, record) => {
|
||||||
|
if (!record.metadata?.labels?.node_uuid) return value
|
||||||
|
return (
|
||||||
|
<Link to={`/cluster/monitor/${clusterID}/nodes/${record.metadata.labels.node_uuid}?_g=${encodeURIComponent(JSON.stringify(param))}`}>
|
||||||
|
{value}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -9,11 +9,13 @@ import Monitor from "@/components/Overview/Monitor";
|
||||||
import StatisticBar from "./statistic_bar";
|
import StatisticBar from "./statistic_bar";
|
||||||
import { Empty } from "antd";
|
import { Empty } from "antd";
|
||||||
import TopN from "./TopN";
|
import TopN from "./TopN";
|
||||||
|
import Logs from "./Logs";
|
||||||
|
|
||||||
const panes = [
|
const panes = [
|
||||||
{ title: "Overview", component: Overview, key: "overview" },
|
{ title: "Overview", component: Overview, key: "overview" },
|
||||||
{ title: "Advanced", component: Advanced, key: "advanced" },
|
{ title: "Advanced", component: Advanced, key: "advanced" },
|
||||||
{ title: "TopN", component: TopN, key: "topn" },
|
{ title: "TopN", component: TopN, key: "topn" },
|
||||||
|
{ title: "Logs", component: Logs, key: "logs" },
|
||||||
{ title: "Nodes", component: Nodes, key: "nodes" },
|
{ title: "Nodes", component: Nodes, key: "nodes" },
|
||||||
{ title: "Indices", component: Indices, key: "indices" },
|
{ title: "Indices", component: Indices, key: "indices" },
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { Empty, Input, Spin, Table } from "antd";
|
||||||
|
import { formatMessage } from "umi/locale";
|
||||||
|
import Logs from "../../components/Logs";
|
||||||
|
|
||||||
|
const AGGS = {
|
||||||
|
"Type":{"terms":{"field":"metadata.name", "size": 1000 }},
|
||||||
|
"Level":{"terms":{"field":"payload.level", "size": 1000 }},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
const { nodeID } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Logs
|
||||||
|
{...props}
|
||||||
|
aggs={AGGS}
|
||||||
|
queryFilters={[
|
||||||
|
{
|
||||||
|
"term": {
|
||||||
|
"metadata.labels.node_uuid": nodeID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -6,10 +6,12 @@ import { formatMessage } from "umi/locale";
|
||||||
import Monitor from "@/components/Overview/Monitor";
|
import Monitor from "@/components/Overview/Monitor";
|
||||||
import StatisticBar from "./statistic_bar";
|
import StatisticBar from "./statistic_bar";
|
||||||
import { connect } from "dva";
|
import { connect } from "dva";
|
||||||
|
import Logs from "./Logs";
|
||||||
|
|
||||||
const panes = [
|
const panes = [
|
||||||
{ title: "Overview", component: Overview, key: "overview" },
|
{ title: "Overview", component: Overview, key: "overview" },
|
||||||
{ title: "Advanced", component: Advanced, key: "advanced" },
|
{ title: "Advanced", component: Advanced, key: "advanced" },
|
||||||
|
{ title: "Logs", component: Logs, key: "logs" },
|
||||||
{ title: "Shards", component: Shards, key: "shards" },
|
{ title: "Shards", component: Shards, key: "shards" },
|
||||||
];
|
];
|
||||||
const Page = (props) => {
|
const Page = (props) => {
|
||||||
|
|
|
@ -0,0 +1,329 @@
|
||||||
|
import { Empty, Input, Spin, Table } from "antd";
|
||||||
|
import styles from "./index.less"
|
||||||
|
import DatePicker from "@/common/src/DatePicker";
|
||||||
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { formatESSearchResult, formatTimeRange } from "@/lib/elasticsearch/util";
|
||||||
|
import { formatMessage } from "umi/locale";
|
||||||
|
import request from "@/utils/request";
|
||||||
|
import moment from "moment";
|
||||||
|
import { getTimezone } from "@/utils/utils";
|
||||||
|
import ListView from "@/components/ListView";
|
||||||
|
import { ESPrefix } from "@/services/common";
|
||||||
|
import Side from "../Side";
|
||||||
|
import { WidgetRender } from "@/pages/DataManagement/View/WidgetLoader";
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
|
import { Link } from "umi";
|
||||||
|
import InstallAgent from "@/components/InstallAgent";
|
||||||
|
|
||||||
|
const COLORS = {
|
||||||
|
'INFO': '#e8eef2',
|
||||||
|
'WARN': '#e99d43',
|
||||||
|
'ERROR': '#ff3f3f'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
const { timeRange, isAgent, refresh, aggs, queryFilters = [], extraColumns = [] } = props;
|
||||||
|
|
||||||
|
const ref = useRef(null);
|
||||||
|
|
||||||
|
const timeField = "timestamp";
|
||||||
|
const indexName = ".infini_logs";
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [result, setResult] = useState(false)
|
||||||
|
|
||||||
|
const [queryParams, setQueryParams] = useState({
|
||||||
|
size: 20,
|
||||||
|
from: 0,
|
||||||
|
sort: {
|
||||||
|
[timeField]: 'desc'
|
||||||
|
},
|
||||||
|
keyword: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchData = async (queryParams, timeRange, aggs, queryFilters) => {
|
||||||
|
if (!timeRange) return;
|
||||||
|
setLoading(true)
|
||||||
|
const newTimeRange = formatTimeRange(timeRange);
|
||||||
|
const filters = [...queryFilters]
|
||||||
|
if (queryParams?.filters) {
|
||||||
|
Object.keys(queryParams.filters).map((field) => {
|
||||||
|
const values = queryParams.filters[field];
|
||||||
|
if (Array.isArray(values)) {
|
||||||
|
values.forEach((item) => {
|
||||||
|
filters.push({
|
||||||
|
'term': { [field]: item }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (values) {
|
||||||
|
filters.push({
|
||||||
|
'term': { [field]: values }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (queryParams?.keyword) {
|
||||||
|
filters.push({
|
||||||
|
query_string: {
|
||||||
|
query: `*${queryParams.keyword}*`,
|
||||||
|
fields: ['payload.message'],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const res = await request(`${ESPrefix}/infini_default_system_cluster/search/ese?timeout=60m`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
index: indexName,
|
||||||
|
body: {
|
||||||
|
aggs,
|
||||||
|
query: {
|
||||||
|
"bool": {
|
||||||
|
"filter": [
|
||||||
|
{
|
||||||
|
"range": {
|
||||||
|
"timestamp": {
|
||||||
|
"gte": newTimeRange.min,
|
||||||
|
"lte": newTimeRange.max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...filters
|
||||||
|
],
|
||||||
|
"should": [],
|
||||||
|
"must_not": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
size: queryParams.size,
|
||||||
|
from: queryParams.from,
|
||||||
|
sort: queryParams.sort ? Object.keys(queryParams.sort).filter((key) => !!queryParams.sort[key]).map((key) => ({
|
||||||
|
[key]: {
|
||||||
|
"order": queryParams.sort[key]
|
||||||
|
}
|
||||||
|
})) : []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (res && !res.error) {
|
||||||
|
const rs = formatESSearchResult(res);
|
||||||
|
setResult(rs)
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTableChange = (pagination, filters, sorter, extra) => {
|
||||||
|
const sortOrder = sorter.order ? sorter.order.replace("end", "") : null;
|
||||||
|
if (queryParams?.sort[sorter.field] !== sortOrder) {
|
||||||
|
setQueryParams((st) => ({ ...st, sort: {
|
||||||
|
...(queryParams.sort || {}),
|
||||||
|
[sorter.field]: sortOrder
|
||||||
|
}}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData(queryParams, timeRange, aggs, queryFilters)
|
||||||
|
}, [JSON.stringify(queryParams), timeRange, JSON.stringify(aggs), JSON.stringify(queryFilters)])
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: formatMessage({ id: "cluster.monitor.logs.timestamp" }),
|
||||||
|
key: timeField,
|
||||||
|
dataIndex: timeField,
|
||||||
|
width: 170,
|
||||||
|
render: (value) =>
|
||||||
|
moment(value)
|
||||||
|
.tz(getTimezone())
|
||||||
|
.format("YYYY-MM-DD HH:mm:ss"),
|
||||||
|
sorter: true,
|
||||||
|
sortOrder: queryParams.sort?.[timeField] ? `${queryParams.sort?.[timeField]}end` : undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: formatMessage({ id: "cluster.monitor.logs.type" }),
|
||||||
|
key: "metadata.name",
|
||||||
|
dataIndex: "metadata.name",
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: formatMessage({ id: "cluster.monitor.logs.level" }),
|
||||||
|
key: "payload.level",
|
||||||
|
dataIndex: "payload.level",
|
||||||
|
width: 88,
|
||||||
|
render: (value, record) => {
|
||||||
|
if (!value) return '-'
|
||||||
|
const colors = {
|
||||||
|
INFO: {
|
||||||
|
background: COLORS.INFO,
|
||||||
|
color: "#959ea0",
|
||||||
|
},
|
||||||
|
WARN: {
|
||||||
|
background: COLORS.WARN,
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
ERROR: {
|
||||||
|
background: COLORS.ERROR,
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 54,
|
||||||
|
height: 18,
|
||||||
|
fontWeight: 600,
|
||||||
|
lineHeight: "18px",
|
||||||
|
textAlign: "center",
|
||||||
|
...(colors[value] || colors[value]),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...extraColumns,
|
||||||
|
{
|
||||||
|
title: formatMessage({ id: "cluster.monitor.logs.message" }),
|
||||||
|
key: "payload.message",
|
||||||
|
dataIndex: "payload.message",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const histogram = {
|
||||||
|
bucket_size: "auto",
|
||||||
|
is_stack: true,
|
||||||
|
format: {
|
||||||
|
type: "number",
|
||||||
|
pattern: "0.00a",
|
||||||
|
},
|
||||||
|
legend: false,
|
||||||
|
colors: COLORS,
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
metric: {
|
||||||
|
formula: "a",
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
field: "payload.level",
|
||||||
|
limit: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
field: "*",
|
||||||
|
name: "a",
|
||||||
|
statistic: "count",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sort: [
|
||||||
|
{
|
||||||
|
direction: "desc",
|
||||||
|
key: "_count",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
queries: {
|
||||||
|
cluster_id: "infini_default_system_cluster",
|
||||||
|
indices: [indexName],
|
||||||
|
time_field: timeField,
|
||||||
|
},
|
||||||
|
type: "date-histogram",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNotEmpty = useMemo(() => {
|
||||||
|
return result?.data?.length > 0
|
||||||
|
}, [result?.data?.length])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<div className={styles.logs} style={isNotEmpty ? {} : { justifyContent: 'center', alignItems: 'center'}}>
|
||||||
|
{
|
||||||
|
isNotEmpty ? (
|
||||||
|
<>
|
||||||
|
<div className={styles.side}>
|
||||||
|
<Side
|
||||||
|
aggs={aggs}
|
||||||
|
data={result?.aggregations || {}}
|
||||||
|
filters={queryParams?.filters || {}}
|
||||||
|
onFacetChange={(v) => {
|
||||||
|
const newFilters = cloneDeep(queryParams.filters) || {}
|
||||||
|
if (!v.value || v.value.length === 0) {
|
||||||
|
delete newFilters[v.field];
|
||||||
|
} else {
|
||||||
|
newFilters[v.field] = v.value;
|
||||||
|
}
|
||||||
|
setQueryParams((st) => ({ ...st, from: 0, filters: newFilters }));
|
||||||
|
}}
|
||||||
|
onReset={() => {
|
||||||
|
setQueryParams((st) => ({ ...st, from: 0, filters: {} }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.result}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<Input.Search
|
||||||
|
style={{ maxWidth: 600 }}
|
||||||
|
placeholder={formatMessage({ id: "cluster.monitor.logs.search.placeholder" })}
|
||||||
|
onSearch={value => {
|
||||||
|
setQueryParams((st) => ({ ...st, from: 0, keyword: value }));
|
||||||
|
}}
|
||||||
|
enterButton
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.histogram}>
|
||||||
|
<WidgetRender
|
||||||
|
widget={histogram}
|
||||||
|
range={{
|
||||||
|
from: timeRange.min,
|
||||||
|
to: timeRange.max,
|
||||||
|
timeField: timeField,
|
||||||
|
}}
|
||||||
|
queryParams={queryParams?.filters || {}}
|
||||||
|
refresh={refresh}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.table}>
|
||||||
|
<Table
|
||||||
|
size={"small"}
|
||||||
|
rowKey={"id"}
|
||||||
|
columns={columns}
|
||||||
|
dataSource={result?.data || []}
|
||||||
|
onChange={onTableChange}
|
||||||
|
pagination={{
|
||||||
|
size: "small",
|
||||||
|
pageSize: queryParams.size,
|
||||||
|
current: Math.ceil(queryParams.from / queryParams.size) + 1,
|
||||||
|
total: result?.total?.value || result?.total || 0,
|
||||||
|
onChange: (page, pageSize) => {
|
||||||
|
setQueryParams((st) => ({
|
||||||
|
...st,
|
||||||
|
from: (page - 1) * st.size,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
showSizeChanger: true,
|
||||||
|
onShowSizeChange: (_, size) => {
|
||||||
|
setQueryParams((st) => ({ ...st, from: 0, size }));
|
||||||
|
},
|
||||||
|
showTotal: (total, range) =>
|
||||||
|
`${range[0]}-${range[1]} of ${total} items`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Empty
|
||||||
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||||
|
description={formatMessage({ id: `cluster.monitor.logs.empty.${isAgent ? 'agent' : 'agentless'}` })}
|
||||||
|
>
|
||||||
|
<div style={{width: 644}}>{!isAgent && <InstallAgent autoInit={false}/>}</div>
|
||||||
|
</Empty>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Spin>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
.logs {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
min-height: calc(100vh - 444px);
|
||||||
|
|
||||||
|
.side {
|
||||||
|
width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
width: calc(100% - 220px - 12px);
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogram {
|
||||||
|
width: 100%;
|
||||||
|
height: 140px;
|
||||||
|
border: 1px solid rgb(232, 232, 232);
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -324,6 +324,19 @@ export default (props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={metricKey} ref={containerRef} className={className} style={style}>
|
<div key={metricKey} ref={containerRef} className={className} style={style}>
|
||||||
|
{
|
||||||
|
metric?.request && (
|
||||||
|
<CopyToClipboard text={`GET .infini_metrics/_search\n${metric.request}`}>
|
||||||
|
<Tooltip title={formatMessage({id: "cluster.metrics.request.copy"})}>
|
||||||
|
<Icon
|
||||||
|
className="copyReq"
|
||||||
|
type="copy"
|
||||||
|
onClick={() => message.success(formatMessage({id: "cluster.metrics.request.copy.success"}))}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</CopyToClipboard>
|
||||||
|
)
|
||||||
|
}
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
<div className={styles.vizChartItemTitle}>
|
<div className={styles.vizChartItemTitle}>
|
||||||
<span>
|
<span>
|
||||||
|
@ -339,20 +352,6 @@ export default (props) => {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
|
||||||
metric?.request && (
|
|
||||||
<CopyToClipboard text={`GET .infini_metrics/_search\n${metric.request}`}>
|
|
||||||
<Tooltip title={formatMessage({id: "cluster.metrics.request.copy"})}>
|
|
||||||
<Icon
|
|
||||||
className={styles.copy}
|
|
||||||
style={{ marginRight: 12 }}
|
|
||||||
type="copy"
|
|
||||||
onClick={() => message.success(formatMessage({id: "cluster.metrics.request.copy.success"}))}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</CopyToClipboard>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<Tooltip title={formatMessage({id: "form.button.refresh"})}>
|
<Tooltip title={formatMessage({id: "form.button.refresh"})}>
|
||||||
<Icon className={styles.copy} type="sync" onClick={() => fetchData(...observerRef.current.deps, true)}/>
|
<Icon className={styles.copy} type="sync" onClick={() => fetchData(...observerRef.current.deps, true)}/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
@ -25,6 +25,28 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vizChartContainer{
|
||||||
|
position: relative;
|
||||||
|
.copyReq {
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
right: 3px;
|
||||||
|
bottom: 0px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
z-index: 11;
|
||||||
|
&:hover {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
.copyReq {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.vizChartItem {
|
.vizChartItem {
|
||||||
background: white !important;
|
background: white !important;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { Checkbox, Icon, Input } from "antd";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { formatMessage } from "umi/locale";
|
||||||
|
import styles from "./SearchFacet.less";
|
||||||
|
|
||||||
|
export default ({ field, label, data = [], onChange, selectedKeys }) => {
|
||||||
|
const [filterData, setFilterData] = useState([]);
|
||||||
|
useEffect(() => {
|
||||||
|
setFilterData(data);
|
||||||
|
}, [data]);
|
||||||
|
const onInputChange = (ev) => {
|
||||||
|
let lowerStr = ev.target.value.toLowerCase();
|
||||||
|
setFilterData(
|
||||||
|
data.filter((item) => {
|
||||||
|
let lowerKey = item.key.toLowerCase();
|
||||||
|
return lowerKey.includes(lowerStr);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelectChange = (item, ev) => {
|
||||||
|
if (ev.target.checked) {
|
||||||
|
if (selectedKeys.indexOf(item.key) > -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newKeys = [...selectedKeys, item.key];
|
||||||
|
if (typeof onChange == "function") {
|
||||||
|
onChange({
|
||||||
|
field,
|
||||||
|
value: newKeys,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const newKeys = selectedKeys.filter((key) => key != item.key);
|
||||||
|
if (typeof onChange == "function") {
|
||||||
|
onChange({
|
||||||
|
field,
|
||||||
|
value: newKeys,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const [showMore, setShowMore] = useState(false);
|
||||||
|
return (
|
||||||
|
<div className={styles.searchFacet}>
|
||||||
|
<div className={`${styles.line} ${styles.label}`}>{label}</div>
|
||||||
|
<div className={styles.line}>
|
||||||
|
<Input
|
||||||
|
placeholder={`${formatMessage({
|
||||||
|
id: "listview.side.filter",
|
||||||
|
})} ${label}`}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.line}>
|
||||||
|
{(showMore ? filterData : filterData.slice(0, 5)).map((item) => {
|
||||||
|
return (
|
||||||
|
<div key={item.key} className={styles.value} title={item.key}>
|
||||||
|
<Checkbox
|
||||||
|
onChange={(v) => {
|
||||||
|
onSelectChange(item, v);
|
||||||
|
}}
|
||||||
|
checked={selectedKeys && selectedKeys.indexOf(item.key) > -1}
|
||||||
|
>
|
||||||
|
{item?.key_as_string ?? item.key}
|
||||||
|
</Checkbox>
|
||||||
|
<div className={styles.count}>{item.doc_count}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{!showMore && filterData.length > 5 ? (
|
||||||
|
<div>
|
||||||
|
<a
|
||||||
|
onClick={() => {
|
||||||
|
setShowMore(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon type="plus" />
|
||||||
|
more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,30 @@
|
||||||
|
.searchFacet {
|
||||||
|
.line {
|
||||||
|
padding: 4px 0;
|
||||||
|
&.label {
|
||||||
|
text-transform: capitalize;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8b9bad;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
:global {
|
||||||
|
label.ant-checkbox-wrapper {
|
||||||
|
width: 152px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.count {
|
||||||
|
margin-left: auto;
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.85em;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { Icon } from "antd";
|
||||||
|
import { useMemo, useEffect, useState, useCallback } from "react";
|
||||||
|
import { formatMessage } from "umi/locale";
|
||||||
|
import SearchFacet from "./SearchFacet";
|
||||||
|
import styles from "./index.less";
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
const { aggs, data, filters, onFacetChange, onReset } = props;
|
||||||
|
|
||||||
|
const facets = useMemo(() => {
|
||||||
|
const fts = Object.keys(data).map((item) => {
|
||||||
|
return {
|
||||||
|
label: item,
|
||||||
|
field: aggs[item].terms.field,
|
||||||
|
buckets: data[item]?.buckets || [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return fts;
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.searchFilter}>
|
||||||
|
<div className={styles.title}>
|
||||||
|
<span>{formatMessage({ id: "listview.side.filter" })}</span>
|
||||||
|
<span
|
||||||
|
className={styles.reload}
|
||||||
|
onClick={onReset}
|
||||||
|
title={formatMessage({ id: "listview.side.filter.reset" })}
|
||||||
|
>
|
||||||
|
<Icon type="reload" style={{ color: "rgba(0, 127, 255, 1)" }} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.facetCnt}>
|
||||||
|
{facets.map((ft) => {
|
||||||
|
if (ft.buckets.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<SearchFacet
|
||||||
|
key={ft.field}
|
||||||
|
label={ft.label}
|
||||||
|
field={ft.field}
|
||||||
|
data={ft.buckets}
|
||||||
|
onChange={onFacetChange}
|
||||||
|
selectedKeys={filters[ft.field] || []}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,18 @@
|
||||||
|
.searchFilter {
|
||||||
|
height: 100%;
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
.reload {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.facetCnt {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
}
|
|
@ -69,6 +69,7 @@ export default (props) => {
|
||||||
}),
|
}),
|
||||||
key: "timestamp",
|
key: "timestamp",
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
defaultSortOrder: 'descend',
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
return formatUtcTimeToLocal(record.timestamp);
|
return formatUtcTimeToLocal(record.timestamp);
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue