add new cluster overview and command manage
This commit is contained in:
parent
4b90b75a39
commit
b8690455c4
|
@ -7,6 +7,8 @@ import (
|
|||
"infini.sh/framework/core/orm"
|
||||
"infini.sh/framework/core/util"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -52,20 +54,55 @@ func (h *APIHandler) HandleQueryCommonCommandAction(w http.ResponseWriter, req *
|
|||
resBody := map[string]interface{}{
|
||||
}
|
||||
|
||||
//title := h.GetParameterOrDefault(req, "title", "")
|
||||
//tag := h.GetParameterOrDefault(req, "search", "")
|
||||
var (
|
||||
title = h.GetParameterOrDefault(req, "title", "")
|
||||
queryDSL = `{"query":{"bool":{"filter":[%s]}}, "size": %d, "from": %d}`
|
||||
strSize = h.GetParameterOrDefault(req, "size", "20")
|
||||
strFrom = h.GetParameterOrDefault(req, "from", "0")
|
||||
filterBuilder = &strings.Builder{}
|
||||
)
|
||||
if title != ""{
|
||||
filterBuilder.WriteString(fmt.Sprintf(`{"prefix":{"title": "%s"}}`, title))
|
||||
}
|
||||
size, _ := strconv.Atoi(strSize)
|
||||
if size <= 0 {
|
||||
size = 20
|
||||
}
|
||||
from, _ := strconv.Atoi(strFrom)
|
||||
if from < 0 {
|
||||
from = 0
|
||||
}
|
||||
|
||||
queryDSL = fmt.Sprintf(queryDSL, filterBuilder.String(), size, from)
|
||||
esClient := elastic.GetClient(h.Config.Elasticsearch)
|
||||
//queryDSL :=[]byte(fmt.Sprintf(`{"query":{"bool":{"must":{"match":{"title":"%s"}}}}}`, title))
|
||||
|
||||
searchRes, err := esClient.SearchWithRawQueryDSL(orm.GetIndexName(elastic.CommonCommand{}),nil)
|
||||
searchRes, err := esClient.SearchWithRawQueryDSL(orm.GetIndexName(elastic.CommonCommand{}), []byte(queryDSL))
|
||||
if err != nil {
|
||||
resBody["error"] = err
|
||||
h.WriteJSON(w, resBody, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
h.WriteJSON(w, searchRes,http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *APIHandler) HandleDeleteCommonCommandAction(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
|
||||
resBody := map[string]interface{}{}
|
||||
id := ps.ByName("cid")
|
||||
esClient := elastic.GetClient(h.Config.Elasticsearch)
|
||||
delRes, err := esClient.Delete(orm.GetIndexName(elastic.CommonCommand{}), "", id, "wait_for")
|
||||
if err != nil {
|
||||
resBody["error"] = err.Error()
|
||||
if delRes!=nil{
|
||||
h.WriteJSON(w, resBody, delRes.StatusCode)
|
||||
}else{
|
||||
h.WriteJSON(w, resBody, http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
elastic.RemoveInstance(id)
|
||||
resBody["_id"] = id
|
||||
resBody["result"] = delRes.Result
|
||||
h.WriteJSON(w, resBody, delRes.StatusCode)
|
||||
}
|
|
@ -44,6 +44,7 @@ func Init(cfg *config.AppConfig) {
|
|||
|
||||
ui.HandleUIMethod(api.POST, path.Join(pathPrefix, "elasticsearch/command"), handler.HandleSaveCommonCommandAction)
|
||||
ui.HandleUIMethod(api.GET, path.Join(pathPrefix, "elasticsearch/command"), handler.HandleQueryCommonCommandAction)
|
||||
ui.HandleUIMethod(api.DELETE, path.Join(pathPrefix, "elasticsearch/command/:cid"), handler.HandleDeleteCommonCommandAction)
|
||||
|
||||
//new api
|
||||
ui.HandleUIMethod(api.GET, path.Join(pathPrefix, "alerting/overview"), alerting.GetAlertOverview)
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package config
|
||||
|
||||
const LastCommitLog = "b8fb6a3, Fri Oct 15 11:41:38 2021 +0800, liugq, console tab v0.1 "
|
||||
const BuildDate = "2021-10-21 13:55:53"
|
||||
const LastCommitLog = "N/A"
|
||||
const BuildDate = "N/A"
|
||||
|
||||
const EOLDate = "2021-12-31 10:10:10"
|
||||
const EOLDate = "N/A"
|
||||
|
||||
const Version = "1.0.0_SNAPSHOT"
|
||||
const Version = "0.0.1-SNAPSHOT"
|
||||
|
|
|
@ -33,6 +33,15 @@ export default [
|
|||
// { path: '/', redirect: '/' },
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
path: '/cluster/overview_new',
|
||||
name: 'overview',
|
||||
component: './Cluster/NewOverview',
|
||||
// hideInMenu: true,
|
||||
routes:[
|
||||
{ path: '/', redirect: '/' },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/cluster/overview',
|
||||
name: 'overview',
|
||||
|
@ -471,6 +480,12 @@ export default [
|
|||
component: './System/Cluster/Form',
|
||||
hideInMenu: true
|
||||
},
|
||||
{
|
||||
path: '/system/command',
|
||||
name: 'command',
|
||||
component: './System/Command/Index',
|
||||
hideInMenu: true
|
||||
},
|
||||
// {
|
||||
// path: '/system/settings',
|
||||
// name: 'settings',
|
||||
|
|
|
@ -99,6 +99,7 @@ const ConsoleWrapper = ({
|
|||
<RequestStatusBar
|
||||
requestInProgress={requestInProgress}
|
||||
selectedCluster={selectedCluster}
|
||||
container={consoleRef}
|
||||
requestResult={
|
||||
lastDatum
|
||||
? {
|
||||
|
|
|
@ -143,6 +143,7 @@ export const RequestStatusBar = ({
|
|||
requestInProgress,
|
||||
requestResult,
|
||||
selectedCluster,
|
||||
container,
|
||||
}:Props) => {
|
||||
let content: React.ReactNode = null;
|
||||
const clusterContent = (<div className="base-info">
|
||||
|
@ -154,11 +155,11 @@ export const RequestStatusBar = ({
|
|||
</div>
|
||||
<div className="info-item">
|
||||
<span>集群地址:</span>
|
||||
{selectedCluster.host}
|
||||
<EuiBadge color="default">{selectedCluster.host}</EuiBadge>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span>版本:</span>
|
||||
{selectedCluster.version}
|
||||
<EuiBadge color="default"> {selectedCluster.version}</EuiBadge>
|
||||
</div>
|
||||
</div>);
|
||||
const [headerInfoVisible, setHeaderInfoVisible] = React.useState(false)
|
||||
|
@ -178,6 +179,7 @@ const [headerInfoVisible, setHeaderInfoVisible] = React.useState(false)
|
|||
<>
|
||||
<div className="status_info">
|
||||
<div className="info-item">
|
||||
<span>响应状态:</span>
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={
|
||||
|
@ -193,6 +195,7 @@ const [headerInfoVisible, setHeaderInfoVisible] = React.useState(false)
|
|||
</EuiToolTip>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span>时延:</span>
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={
|
||||
|
@ -227,7 +230,8 @@ const [headerInfoVisible, setHeaderInfoVisible] = React.useState(false)
|
|||
<Drawer title="Request header info"
|
||||
style={{zIndex:1004}}
|
||||
width={520}
|
||||
|
||||
mask={false}
|
||||
// getContainer={container.current}
|
||||
destroyOnClose={true}
|
||||
visible={headerInfoVisible}
|
||||
onClose={()=>{setHeaderInfoVisible(false)}}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import * as React from 'react';
|
||||
import {Tabs} from 'antd';
|
||||
import Clusters from './components/clusters';
|
||||
const {TabPane} = Tabs;
|
||||
|
||||
|
||||
const panes = [
|
||||
{ title: 'Clusters', component: Clusters, key: 'clusters' },
|
||||
{ title: 'Hosts', component: 'Content of Tab 2', key: 'hosts' },
|
||||
{title: 'Nodes', component: 'Content of Tab 3',key: 'nodes'},
|
||||
{title: 'Indices', component: 'Content of Tab 3',key: 'indices'},
|
||||
];
|
||||
|
||||
const NewOverview = ()=>{
|
||||
|
||||
return (<div style={{background:'#fff'}} className="overview">
|
||||
<div>
|
||||
<Tabs
|
||||
onChange={()=>{}}
|
||||
type="card"
|
||||
tabBarGutter={10}
|
||||
>
|
||||
{panes.map(pane => (
|
||||
<TabPane tab={pane.title} key={pane.key}>
|
||||
{typeof pane.component == 'string'? pane.component: <pane.component/>}
|
||||
</TabPane>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
export default NewOverview;
|
|
@ -0,0 +1,63 @@
|
|||
import './cluster_card.scss';
|
||||
|
||||
const ClusterCard = ()=>{
|
||||
return (
|
||||
<div class="cluster-item">
|
||||
<div class="cluster-name">
|
||||
<span>Cluster A</span>
|
||||
</div>
|
||||
<div class="cluster-info">
|
||||
<div class="info-top">
|
||||
<span class="text">Availability History(Last 7 Days)</span>
|
||||
</div>
|
||||
<div class="info-middle">
|
||||
<span class="label">100.0%</span>
|
||||
<span class="label label-primary">128 Nodes</span>
|
||||
<span class="label label-primary">1280 Shards</span>
|
||||
</div>
|
||||
<div class="info-bottom">
|
||||
<span class="status-block bg-green"></span>
|
||||
<span class="status-block bg-green"></span>
|
||||
<span class="status-block bg-yellow"></span>
|
||||
<span class="status-block bg-green"></span>
|
||||
<span class="status-block bg-green"></span>
|
||||
<span class="status-block bg-yellow"></span>
|
||||
<span class="status-block bg-red"></span>
|
||||
<span class="status-block bg-red"></span>
|
||||
<span class="status-block bg-green"></span>
|
||||
<span class="status-block bg-green"></span>
|
||||
<span class="status-block bg-green"></span>
|
||||
<span class="status-block bg-yellow"></span>
|
||||
<span class="status-block bg-red"></span>
|
||||
<span class="status-block bg-green"></span>
|
||||
<span class="status-block bg-yellow"></span>
|
||||
<span class="status-block bg-red"></span>
|
||||
<span class="status-block bg-red"></span>
|
||||
<span class="status-block bg-green"></span>
|
||||
<span class="status-block bg-yellow"></span>
|
||||
<span class="status-block bg-red"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cluster-chart">
|
||||
<div class="chart-top">
|
||||
<div class="pie-chart">
|
||||
<div class="item pie1"></div>
|
||||
<div class="item pie2"></div>
|
||||
</div>
|
||||
<div class="line-chart">
|
||||
<div class="item line1"></div>
|
||||
<div class="item line2"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-bottom">
|
||||
<span class="label label-primary">100TB</span>
|
||||
<span class="label label-primary">Dev</span>
|
||||
<span class="label label-primary">7.6.1</span>
|
||||
<span class="label label-primary">8 Nodes</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ClusterCard;
|
|
@ -0,0 +1,124 @@
|
|||
.bg-green {
|
||||
background-color: green;
|
||||
}
|
||||
.bg-yellow {
|
||||
background-color: yellow;
|
||||
}
|
||||
.bg-red {
|
||||
background-color: red;
|
||||
}
|
||||
.cluster-item {
|
||||
border: 1px solid black;
|
||||
width: 100%;
|
||||
height: 130px;
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
}
|
||||
.cluster-item .cluster-name {
|
||||
width: 15%;
|
||||
height: 100%;
|
||||
background-color: #a9b108;
|
||||
color: white;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.cluster-item .cluster-info {
|
||||
/*background-color: lightblue;*/
|
||||
width: 40%;
|
||||
height: 100%;
|
||||
padding: 0 5px;
|
||||
}
|
||||
.cluster-item .cluster-info .info-top {
|
||||
/*background-color: lightseagreen;*/
|
||||
margin: 5px 0 5px 5px;
|
||||
}
|
||||
.cluster-item .cluster-info .info-top .text {
|
||||
height: 23px;
|
||||
line-height: 23px;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
padding-left: 5px
|
||||
;
|
||||
}
|
||||
.cluster-item .cluster-info .info-middle {
|
||||
width: 100%;
|
||||
margin: 8px auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
}
|
||||
.cluster-item .label {
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
padding: 0 10px;
|
||||
min-width: 60px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
.cluster-item .label.label-primary {
|
||||
background-color: #1f63af;
|
||||
color: white;
|
||||
}
|
||||
.cluster-item .cluster-info .info-bottom {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 3px;
|
||||
}
|
||||
.cluster-item .cluster-info .info-bottom .status-block {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin: 3px 5px;
|
||||
}
|
||||
.cluster-item .cluster-chart {
|
||||
width: 45%;
|
||||
height: 100%;
|
||||
padding: 0 5px;
|
||||
;
|
||||
}
|
||||
.cluster-item .cluster-chart .chart-top {
|
||||
height: 90px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.cluster-item .cluster-chart .chart-top .pie-chart {
|
||||
height: 90px;
|
||||
width: 50%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: lightgray;
|
||||
}
|
||||
.cluster-item .cluster-chart .chart-top .pie-chart .item {
|
||||
width: 50%;
|
||||
}
|
||||
.cluster-item .cluster-chart .chart-top .pie-chart .item.pie1{
|
||||
background-color: goldenrod;
|
||||
}
|
||||
.cluster-item .cluster-chart .chart-top .pie-chart .item.pie2{
|
||||
background-color: greenyellow;
|
||||
}
|
||||
.cluster-item .cluster-chart .chart-top .line-chart {
|
||||
height: 90px;
|
||||
width: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.cluster-item .cluster-chart .chart-top .line-chart .item {
|
||||
height: 45px;
|
||||
width: 100%;
|
||||
}
|
||||
.cluster-item .cluster-chart .chart-top .line-chart .item.line1{
|
||||
background-color: #ff8080;
|
||||
}
|
||||
.cluster-item .cluster-chart .chart-top .line-chart .item.line2{
|
||||
background-color: lightseagreen;
|
||||
}
|
||||
.cluster-item .cluster-chart .chart-bottom {
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
/*background-color: lightgreen;*/
|
||||
justify-content: space-between;
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import {Tabs} from 'antd';
|
||||
import {Metrics} from './detail';
|
||||
|
||||
const {TabPane} = Tabs;
|
||||
|
||||
const panes = [
|
||||
{ title: 'Metrics', component: Metrics, key: 'metrics' },
|
||||
{ title: 'Infos', component: 'Content of Tab 2', key: 'infos' },
|
||||
{title: 'Activities', component: 'Content of Tab 3',key: 'activities'},
|
||||
{title: 'Console', component: 'Content of Tab 3',key: 'console'},
|
||||
];
|
||||
|
||||
const ClusterDetail = ()=>{
|
||||
return (
|
||||
<div>
|
||||
<Tabs
|
||||
onChange={()=>{}}
|
||||
type="card"
|
||||
tabBarGutter={10}
|
||||
tabPosition="right"
|
||||
>
|
||||
{panes.map(pane => (
|
||||
<TabPane tab={pane.title} key={pane.key}>
|
||||
{typeof pane.component == 'string'? pane.component: <pane.component/>}
|
||||
</TabPane>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClusterDetail;
|
|
@ -0,0 +1,73 @@
|
|||
import { Input, Icon, List, Card,Button } from 'antd';
|
||||
import * as React from 'react';
|
||||
import './clusters.scss';
|
||||
import ClusterDetail from './cluster_detail';
|
||||
import {TagList} from './tag';
|
||||
import ClusterCard from './cluster_card';
|
||||
|
||||
const { Search } = Input;
|
||||
|
||||
const Clusters = ()=>{
|
||||
const [collapse, setCollapse] = React.useState(false);
|
||||
const toggleCollapse = ()=>{
|
||||
setCollapse(!collapse)
|
||||
}
|
||||
const clusterList = [1, 2, 3, 4, 5, 6, 7, 8];
|
||||
return (
|
||||
<div className="clusters">
|
||||
<div className="wrapper">
|
||||
<div className={"col left" + (collapse ? " collapse": "")}>
|
||||
<div className="search-line">
|
||||
<div className="search-box">
|
||||
<Search
|
||||
placeholder="search"
|
||||
enterButton="Search"
|
||||
onSearch={value => console.log(value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="help">
|
||||
<Button type="link">Get help?</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="tag-line">
|
||||
<TagList value={[{text:"Dev"}, {text:'7.9.2', checked: true}, {text:"Prod"}, {text:"QA"}, {text:"Metrics"}]} />
|
||||
</div>
|
||||
<div className="card-cnt">
|
||||
<List itemLayout="vertical"
|
||||
size="small"
|
||||
bordered={false}
|
||||
pagination={{
|
||||
onChange: page => {
|
||||
console.log(page);
|
||||
},
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['5','10','20'],
|
||||
showTotal: (total, range) => `${range[0]}-${range[1]} of ${total} items`,
|
||||
pageSize: 5,
|
||||
}}
|
||||
dataSource={clusterList}
|
||||
renderItem={(item)=>(
|
||||
<List.Item>
|
||||
{/* <Card>
|
||||
<p>Card content</p>
|
||||
</Card> */}
|
||||
<ClusterCard/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="collapse" >
|
||||
<span className="area" onClick={toggleCollapse}>
|
||||
<Icon type={collapse ? "right": "left"} className="icon"/>
|
||||
</span>
|
||||
</div>
|
||||
<div className="col right">
|
||||
<ClusterDetail/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Clusters;
|
|
@ -0,0 +1,72 @@
|
|||
.overview {
|
||||
padding: 15px;
|
||||
.clusters{
|
||||
>.wrapper{
|
||||
display: flex;
|
||||
.col{
|
||||
flex: 1 1 50%;
|
||||
position: relative;
|
||||
&.left{
|
||||
border-right: 1px solid rgb(232, 232, 232);
|
||||
padding-right: 15px;
|
||||
.card-cnt{
|
||||
margin-top: 10px;
|
||||
.ant-list-item{
|
||||
border-bottom: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.right{
|
||||
padding-left: 15px;
|
||||
}
|
||||
&.collapse{
|
||||
flex: 0 0 0px;
|
||||
max-width: 0px;
|
||||
min-width: 0px;
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
overflow: hidden;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
>.collapse{
|
||||
margin: auto;
|
||||
.area{
|
||||
font-size: 12px;
|
||||
display: inline-block;
|
||||
color: #6c7f90;
|
||||
z-index: 4;
|
||||
left: -1px;
|
||||
width: 22px;
|
||||
cursor: pointer;
|
||||
transition: color .3s;
|
||||
background-color: #fff;
|
||||
height: 66px;
|
||||
box-sizing: border-box;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
border: 1px solid #ececec;
|
||||
border-left: none;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
.icon{
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
top: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.search-line{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.search-box{
|
||||
flex: 1 1 auto;
|
||||
max-width: 600px;
|
||||
}
|
||||
}
|
||||
.tag-line{
|
||||
margin: 10px auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './metrics';
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
export const Metrics = ()=>{
|
||||
return (
|
||||
<div>
|
||||
Metrics content
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
|
||||
import './tag.scss';
|
||||
import * as React from 'react';
|
||||
|
||||
export const Tag = (props={})=>{
|
||||
|
||||
const [checked, setChecked] = React.useState(props.checked)
|
||||
const toggleChecked = ()=>{
|
||||
if(typeof props.onChange == 'function'){
|
||||
props.onChange({
|
||||
checked: !checked,
|
||||
text: props.text,
|
||||
})
|
||||
}
|
||||
setChecked(!checked);
|
||||
}
|
||||
return (
|
||||
<div className={"tag" +(checked ? ' checked': '')} onClick={toggleChecked}>
|
||||
<div className="wrapper">
|
||||
<span className="text">{props.text}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const TagList = (props)=>{
|
||||
const [value, setValue] = React.useState(()=>{
|
||||
return (props.value||[]).filter(item=>item.checked == true)
|
||||
})
|
||||
const onTagChange = (citem)=>{
|
||||
const newVal = [...value]
|
||||
const idx = newVal.findIndex(item=>item.text == citem.text);
|
||||
if(idx > -1) {
|
||||
if(citem.checked == true){
|
||||
newVal[idx].checked = citem.checked;
|
||||
}else{
|
||||
newVal.splice(idx,1)
|
||||
}
|
||||
}else{
|
||||
if(citem.checked == true){
|
||||
newVal.push(citem);
|
||||
}
|
||||
}
|
||||
if(typeof props.onChange == 'function'){
|
||||
props.onChange(newVal);
|
||||
}
|
||||
setValue(newVal);
|
||||
}
|
||||
return (
|
||||
<div className="tag-list">
|
||||
{(props.value||[]).map((item)=>{
|
||||
return <Tag key={item.text} {...item} onChange={onTagChange} />
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
.tag-list{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.tag{
|
||||
margin-left: 10px;
|
||||
display: inline-block;
|
||||
border: 1px solid #e8e8e8;
|
||||
cursor: pointer;
|
||||
&.checked{
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
>.wrapper{
|
||||
font-size: 12px;
|
||||
line-height: 2em;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
.tag:first-child{
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,331 @@
|
|||
import React, { PureComponent, Fragment } from 'react';
|
||||
import { connect } from 'dva';
|
||||
import {Link} from 'umi';
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
Card,
|
||||
Form,
|
||||
Input,
|
||||
Button,
|
||||
Modal,
|
||||
message,
|
||||
Divider,
|
||||
Drawer,
|
||||
Tabs,
|
||||
Descriptions,
|
||||
Menu,
|
||||
Table,
|
||||
Dropdown,
|
||||
Icon, Popconfirm,
|
||||
Switch,
|
||||
} from 'antd';
|
||||
import Editor from '@monaco-editor/react';
|
||||
|
||||
import styles from '../../List/TableList.less';
|
||||
import {transformSettingsForApi} from '@/lib/elasticsearch/edit_settings';
|
||||
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
const { TextArea } = Input;
|
||||
const {TabPane} = Tabs;
|
||||
|
||||
class JSONWrapper extends PureComponent {
|
||||
state ={
|
||||
height: 400,
|
||||
}
|
||||
componentDidMount(){
|
||||
|
||||
let getElementTop = (elem)=>{
|
||||
var elemTop=elem.offsetTop;
|
||||
elem=elem.offsetParent;
|
||||
|
||||
while(elem!=null){
|
||||
elemTop+=elem.offsetTop;
|
||||
elem=elem.offsetParent;
|
||||
}
|
||||
|
||||
return elemTop;
|
||||
|
||||
}
|
||||
// console.log(getElementTop(this.refs.jsonw));
|
||||
this.setState({height: window.innerHeight - getElementTop(this.refs.jsonw) -50});
|
||||
}
|
||||
render(){
|
||||
return (
|
||||
<div id="jsonw" ref="jsonw" onClick={()=>{console.log(document.getElementById('jsonw').offsetTop)}} style={{overflow:"scroll", height: this.state.height}}> {this.props.children}</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@Form.create()
|
||||
class CreateForm extends React.Component {
|
||||
okHandle = () => {
|
||||
const {handleAdd, form} = this.props;
|
||||
const me = this;
|
||||
|
||||
form.validateFields((err, fieldsValue) => {
|
||||
if (err) return;
|
||||
fieldsValue['config'] = me.editor.getValue();
|
||||
handleAdd(fieldsValue);
|
||||
form.resetFields();
|
||||
});
|
||||
};
|
||||
onEditorDidMount = (editor)=>{
|
||||
this.editor = editor;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {modalVisible, form, handleModalVisible} = this.props;
|
||||
return (
|
||||
<Modal
|
||||
destroyOnClose
|
||||
title="新建索引"
|
||||
visible={modalVisible}
|
||||
width={640}
|
||||
onOk={this.okHandle}
|
||||
onCancel={() => handleModalVisible()}
|
||||
>
|
||||
<FormItem labelCol={{span: 5}} wrapperCol={{span: 15}} label="索引名称">
|
||||
{form.getFieldDecorator('index', {
|
||||
rules: [{required: true, message: '请输入至少五个字符的名称!', min: 5}],
|
||||
})(<Input placeholder="请输入名称"/>)}
|
||||
</FormItem>
|
||||
<FormItem labelCol={{span: 5}} wrapperCol={{span: 15}} label="索引设置">
|
||||
<div style={{border: '1px solid rgb(232, 232, 232)'}}>
|
||||
<Editor
|
||||
height="300px"
|
||||
language="json"
|
||||
theme="light"
|
||||
options={{
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
tabSize: 2,
|
||||
wordBasedSuggestions: true,
|
||||
}}
|
||||
onMount={this.onEditorDidMount}
|
||||
/>
|
||||
</div>
|
||||
</FormItem>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* eslint react/no-multi-comp:0 */
|
||||
@connect(({ command }) => ({
|
||||
command
|
||||
}))
|
||||
@Form.create()
|
||||
class Index extends PureComponent {
|
||||
state = {
|
||||
modalVisible: false,
|
||||
updateModalVisible: false,
|
||||
expandForm: false,
|
||||
formValues: {},
|
||||
drawerVisible: false,
|
||||
editingCommand:{},
|
||||
indexActiveKey: '1',
|
||||
showSystemIndices: false,
|
||||
};
|
||||
columns = [
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'title',
|
||||
render: (text, record) => (
|
||||
<a onClick={()=>{
|
||||
this.setState({
|
||||
editingCommand: record,
|
||||
drawerVisible: true,
|
||||
});
|
||||
}}>{text}</a>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '标签',
|
||||
dataIndex: 'tag',
|
||||
// render: (val)=>{
|
||||
// return val || 0;
|
||||
// }
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
render: (text, record) => (
|
||||
<Fragment>
|
||||
<Popconfirm title="Sure to delete?" onConfirm={() => this.handleDeleteClick(record.id)}>
|
||||
<a>删除</a>
|
||||
</Popconfirm>
|
||||
</Fragment>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchData()
|
||||
}
|
||||
|
||||
fetchData = (params={})=>{
|
||||
const { dispatch } = this.props;
|
||||
dispatch({
|
||||
type: 'command/fetchCommandList',
|
||||
payload: {
|
||||
...params
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleFormReset = () => {
|
||||
const { form, dispatch } = this.props;
|
||||
form.resetFields();
|
||||
this.setState({
|
||||
formValues: {},
|
||||
});
|
||||
};
|
||||
|
||||
handleDeleteClick = (id) => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch({
|
||||
type: 'command/removeCommand',
|
||||
payload: {
|
||||
id: id,
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
handleSearch = e => {
|
||||
e.preventDefault();
|
||||
const { dispatch, form } = this.props;
|
||||
|
||||
form.validateFields((err, fieldsValue) => {
|
||||
if (err) return;
|
||||
this.fetchData({
|
||||
title: fieldsValue.name,
|
||||
from: 0,
|
||||
size: 10,
|
||||
})
|
||||
this.setState({
|
||||
searchKey: fieldsValue.name,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
handleModalVisible = flag => {
|
||||
this.setState({
|
||||
modalVisible: !!flag,
|
||||
});
|
||||
};
|
||||
|
||||
handleIndexTabChanged = (activeKey, indexName) => {
|
||||
}
|
||||
handleEditorDidMount = (editorName, editor)=>{
|
||||
this[editorName] = editor;
|
||||
}
|
||||
|
||||
handleIndexSettingsSaveClick = (indexName)=>{
|
||||
}
|
||||
buildRawCommonCommandRequest(cmd){
|
||||
const {requests} = cmd;
|
||||
if(!requests){
|
||||
return '';
|
||||
}
|
||||
const strReqs = requests.map((req)=>{
|
||||
const {method, path, body} = req;
|
||||
return `${method} ${path}\n${body}`;
|
||||
})
|
||||
return strReqs.join('\n');
|
||||
}
|
||||
handleRereshClick=()=>{
|
||||
const {searchKey} = this.state;
|
||||
this.fetchData({
|
||||
title: searchKey,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {data, total} = this.props.command;
|
||||
const { modalVisible, updateModalVisible, updateFormValues, drawerVisible, editingCommand } = this.state;
|
||||
const parentMethods = {
|
||||
handleAdd: this.handleAdd,
|
||||
handleModalVisible: this.handleModalVisible,
|
||||
};
|
||||
const updateMethods = {
|
||||
handleUpdateModalVisible: this.handleUpdateModalVisible,
|
||||
handleUpdate: this.handleUpdate,
|
||||
};
|
||||
const {form: { getFieldDecorator }} = this.props;
|
||||
|
||||
return (
|
||||
<PageHeaderWrapper>
|
||||
<Card bordered={false}>
|
||||
<div className={styles.tableList}>
|
||||
<div className={styles.tableListForm}>
|
||||
<Form onSubmit={this.handleSearch} layout="inline">
|
||||
<Row gutter={{ md: 8, lg: 24, xl: 48 }}>
|
||||
<Col md={8} sm={24}>
|
||||
<FormItem label="名称">
|
||||
{getFieldDecorator('name')(<Input placeholder="请输入" />)}
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col md={8} sm={24}>
|
||||
<span className={styles.submitButtons}>
|
||||
<Button type="primary" htmlType="submit">
|
||||
查询
|
||||
</Button>
|
||||
<Button style={{ marginLeft: 8 }} onClick={this.handleFormReset}>
|
||||
重置
|
||||
</Button>
|
||||
</span>
|
||||
</Col>
|
||||
<Col md={8} sm={24} style={{textAlign:"right"}}>
|
||||
<Button icon="redo" style={{marginRight:10}} onClick={this.handleRereshClick}>刷新</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<Table bordered
|
||||
dataSource={data}
|
||||
rowKey='id'
|
||||
pagination={
|
||||
{pageSize: 10,}
|
||||
}
|
||||
columns={this.columns}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<CreateForm {...parentMethods} modalVisible={modalVisible} />
|
||||
<Drawer title={editingCommand.title}
|
||||
visible={drawerVisible}
|
||||
onClose={()=>{
|
||||
this.setState({
|
||||
drawerVisible: false,
|
||||
indexActiveKey: '1',
|
||||
});
|
||||
}}
|
||||
width={720}
|
||||
>
|
||||
<div style={{border: '1px solid rgb(232, 232, 232)'}}>
|
||||
<Editor
|
||||
height="300px"
|
||||
language="text"
|
||||
theme="light"
|
||||
value={this.buildRawCommonCommandRequest(editingCommand)}
|
||||
options={{
|
||||
readOnly: true,
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
tabSize: 2,
|
||||
wordBasedSuggestions: true,
|
||||
}}
|
||||
onMount={(editor)=>this.handleEditorDidMount('indexSettingsEditor', editor)}
|
||||
/>
|
||||
</div>
|
||||
</Drawer>
|
||||
</PageHeaderWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Index;
|
|
@ -0,0 +1,54 @@
|
|||
import {searchCommand, deleteCommand} from "@/services/command";
|
||||
import {message} from "antd";
|
||||
import {formatESSearchResult} from '@/lib/elasticsearch/util';
|
||||
|
||||
export default {
|
||||
namespace: 'command',
|
||||
state: {
|
||||
},
|
||||
effects:{
|
||||
*fetchCommandList({payload}, {call, put, select}){
|
||||
let res = yield call(searchCommand, payload);
|
||||
if(res.error){
|
||||
message.error(res.error)
|
||||
return false;
|
||||
}
|
||||
res = formatESSearchResult(res)
|
||||
yield put({
|
||||
type: 'saveData',
|
||||
payload: res
|
||||
})
|
||||
},
|
||||
|
||||
*removeCommand({payload}, {call, put, select}) {
|
||||
let res = yield call(deleteCommand, payload)
|
||||
if(res.error){
|
||||
message.error(res.error)
|
||||
return false;
|
||||
}
|
||||
let {data, total} = yield select(state => state.command);
|
||||
data = data.filter((item)=>{
|
||||
return item.id !== payload.id;
|
||||
})
|
||||
yield put({
|
||||
type: 'saveData',
|
||||
payload: {
|
||||
data,
|
||||
total: {
|
||||
...total,
|
||||
value: total.value - 1
|
||||
}
|
||||
}
|
||||
})
|
||||
return res;
|
||||
},
|
||||
},
|
||||
reducers:{
|
||||
saveData(state, {payload}){
|
||||
return {
|
||||
...state,
|
||||
...payload,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import request from '@/utils/request';
|
||||
import {buildQueryArgs, pathPrefix} from './common';
|
||||
|
||||
export async function searchCommand(params) {
|
||||
let url = `${pathPrefix}/elasticsearch/command`;
|
||||
let args = buildQueryArgs({
|
||||
title: params.title,
|
||||
from: params.from,
|
||||
size: params.size,
|
||||
});
|
||||
if(args.length > 0){
|
||||
url += args;
|
||||
}
|
||||
return request(url, {
|
||||
method: 'GET'
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteCommand(params) {
|
||||
const url = `${pathPrefix}/elasticsearch/command/${params.id}`;
|
||||
return request(url, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue