console of adding muti tab

This commit is contained in:
liugq 2021-10-21 14:12:15 +08:00
parent b8fb6a3d87
commit 5fcaed73ff
31 changed files with 784 additions and 147 deletions

View File

@ -0,0 +1,71 @@
package index_management
import (
"fmt"
httprouter "infini.sh/framework/core/api/router"
"infini.sh/framework/core/elastic"
"infini.sh/framework/core/orm"
"infini.sh/framework/core/util"
"net/http"
"time"
)
func (h *APIHandler) HandleSaveCommonCommandAction(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
resBody := map[string]interface{}{
}
reqParams := elastic.CommonCommand{}
err := h.DecodeJSON(req, &reqParams)
if err != nil {
resBody["error"] = err
h.WriteJSON(w, resBody, http.StatusInternalServerError)
return
}
reqParams.Created = time.Now()
reqParams.ID = util.GetUUID()
esClient := elastic.GetClient(h.Config.Elasticsearch)
queryDSL :=[]byte(fmt.Sprintf(`{"size":1, "query":{"bool":{"must":{"match":{"title":"%s"}}}}}`, reqParams.Title))
var indexName = orm.GetIndexName(reqParams)
searchRes, err := esClient.SearchWithRawQueryDSL(indexName, queryDSL)
if err != nil {
resBody["error"] = err
h.WriteJSON(w, resBody, http.StatusInternalServerError)
return
}
if len(searchRes.Hits.Hits) > 0 {
resBody["error"] = "title already exists"
h.WriteJSON(w, resBody, http.StatusInternalServerError)
return
}
_, err = esClient.Index(indexName,"", reqParams.ID, reqParams)
resBody["_id"] = reqParams.ID
resBody["result"] = "created"
resBody["_source"] = reqParams
h.WriteJSON(w, resBody,http.StatusOK)
}
func (h *APIHandler) HandleQueryCommonCommandAction(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
resBody := map[string]interface{}{
}
//title := h.GetParameterOrDefault(req, "title", "")
//tag := h.GetParameterOrDefault(req, "search", "")
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)
if err != nil {
resBody["error"] = err
h.WriteJSON(w, resBody, http.StatusInternalServerError)
return
}
h.WriteJSON(w, searchRes,http.StatusOK)
}

View File

@ -42,6 +42,9 @@ func Init(cfg *config.AppConfig) {
ui.HandleUIMethod(api.DELETE, path.Join(esPrefix, "index/:index"), handler.HandleDeleteIndexAction)
ui.HandleUIMethod(api.POST, path.Join(esPrefix, "index/:index"), handler.HandleCreateIndexAction)
ui.HandleUIMethod(api.POST, path.Join(pathPrefix, "elasticsearch/command"), handler.HandleSaveCommonCommandAction)
ui.HandleUIMethod(api.GET, path.Join(pathPrefix, "elasticsearch/command"), handler.HandleQueryCommonCommandAction)
//new api
ui.HandleUIMethod(api.GET, path.Join(pathPrefix, "alerting/overview"), alerting.GetAlertOverview)
ui.HandleUIMethod(api.GET, path.Join(pathPrefix, "alerting/overview/alerts"), alerting.GetAlerts)
@ -74,6 +77,7 @@ func Init(cfg *config.AppConfig) {
ui.HandleUIMethod(api.GET, "/elasticsearch/:id/alerting/alerts", alerting.GetAlerts)
ui.HandleUIMethod(api.POST, "/elasticsearch/:id/alerting/_monitors/:monitorID/_acknowledge/alerts", alerting.AcknowledgeAlerts)
task.RegisterScheduleTask(task.ScheduleTask{
Description: "sync reindex task result",
Task: func() {

View File

@ -1,8 +1,8 @@
package config
const LastCommitLog = "N/A"
const BuildDate = "N/A"
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 EOLDate = "N/A"
const EOLDate = "2021-12-31 10:10:10"
const Version = "0.0.1-SNAPSHOT"
const Version = "1.0.0_SNAPSHOT"

View File

@ -0,0 +1,26 @@
.fullscreen{
position: fixed;
width: 100%;
left: 0;
top: 0;
height: 100vh;
z-index: 100;
}
@keyframes slideInUp {
from {
transform: translate3d(0, 100%, 0);
visibility: visible;
}
to {
transform: translate3d(0, 0, 0);
}
}
.slideInUp {
animation-name: slideInUp;
animation-duration: .2s;
animation-fill-mode: both;
animation-timing-function: cubic-bezier(0.55, 0, 0.55, 0.2);
}

View File

@ -7,8 +7,12 @@ import NoticeIcon from '../NoticeIcon';
import HeaderSearch from '../HeaderSearch';
import SelectLang from '../SelectLang';
import styles from './index.less';
import {ConsoleUI} from '@/pages/DevTool/Console';
import { Resizable } from "re-resizable";
import {ResizeBar} from '@/components/infini/resize_bar';
export default class GlobalHeaderRight extends PureComponent {
state={consoleVisible: false}
getNoticeData() {
const { notices = [] } = this.props;
if (notices.length === 0) {
@ -95,7 +99,10 @@ export default class GlobalHeaderRight extends PureComponent {
<a className={styles.action} onClick={()=>{
const {history, selectedCluster} = this.props;
history.push(`/dev_tool/elasticsearch/${selectedCluster.id}/`);
// history.push(`/dev_tool`);
this.setState({
consoleVisible: !this.state.consoleVisible
})
}}> <Icon type="code" /></a>
{/* <NoticeIcon
@ -151,7 +158,52 @@ export default class GlobalHeaderRight extends PureComponent {
<Spin size="small" style={{ marginLeft: 8, marginRight: 8 }} />
)} */}
<SelectLang className={styles.action} />
<div style={{
display: this.state.consoleVisible ? 'block': 'none',
borderTop: "solid 1px #ddd",
background: "#fff",
position: "fixed",
left: 0,
right: 0,
bottom: 0,
zIndex: 1002,
}}>
{/* <Resizable
defaultSize={{
height: '50vh'
}}
minHeight={200}
maxHeight="100vh"
handleComponent={{ top: <ResizeBar/> }}
enable={{
top: true,
right: false,
bottom: false,
left: false,
topRight: false,
bottomRight: false,
bottomLeft: false,
topLeft: false,
}}> */}
<ConsoleUI selectedCluster={this.props.selectedCluster}
clusterList={this.props.clusterList}
visible={false}
minimize={true}
onMinimizeClick={()=>{
this.setState({
consoleVisible: false,
})
}}
clusterStatus={this.props.clusterStatus}
resizeable={true}
/>
{/* </Resizable> */}
</div>
</div>
);
}
}
const TopHandle = () => {
return <div style={{ background: "red" }}>hello world</div>;
};

View File

@ -1,4 +1,5 @@
export type ClusterHealthStatus = 'green' | 'yellow' | 'red';
import {Icon} from 'antd';
export type ClusterHealthStatus = 'green' | 'yellow' | 'red' | 'unavailable';
const statusColorMap: Record<string, string> = {
'green': '#39b362',
@ -15,6 +16,9 @@ interface props {
}
export const HealthStatusCircle = ({status}: props)=>{
if(status == 'unavailable'){
return <Icon type="close-circle" style={{width:14, height:14, color:'red',borderRadius: 14, boxShadow: '0px 0px 5px #555'}}/>
}
const color = convertStatusToColor(status);
return <div style={{background: color, height:14, width:14, borderRadius: 14, boxShadow: '0px 0px 5px #999', display: 'inline-block'}}></div>
}

View File

@ -0,0 +1,9 @@
import './resize_bar.scss';
export const ResizeBar = () => {
return <div className="resize-bar">
<div>
<div className="dash"></div>
</div>
</div>;
};

View File

@ -0,0 +1,13 @@
.resize-bar{
display: flex;
height: 10px;
background: #eee;
border-top: 1px solid #bbb;
align-items: center;
justify-content: center;
.dash{
height: 2px;
width: 24px;
background: #999;
}
}

View File

@ -66,7 +66,7 @@ const CommonCommandModal = Form.create()((props: ICommonCommandModalProps) => {
};
return (
<Modal title="保存常用命令" visible={true} onCancel={props.onClose} onOk={handleConfirm} cancelText="取消" okText="确认">
<Modal title="保存常用命令" visible={true} onCancel={props.onClose} onOk={handleConfirm} zIndex={1003} cancelText="取消" okText="确认">
<Form layout="vertical">
<Form.Item label="标题">
{form.getFieldDecorator('title', {

View File

@ -1,5 +1,5 @@
// @ts-ignore
import React, { useRef, useMemo,useEffect } from 'react';
import React, { useRef, useMemo,useEffect, useLayoutEffect } from 'react';
import ConsoleInput from './ConsoleInput';
import ConsoleOutput from './ConsoleOutput';
import { Panel } from './Panel';
@ -14,12 +14,14 @@ import { createHistory, History, createStorage, createSettings } from '../servic
import { create } from '../storage/local_storage_object_client';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import {RequestStatusBar} from './request_status_bar';
import useEventListener from '@/lib/hooks/use_event_listener';
interface props {
selectedCluster: any;
saveEditorContent: (content: string)=>void;
initialText: string;
paneKey: string;
height: number;
}
const INITIAL_PANEL_WIDTH = 50;
@ -30,8 +32,8 @@ const ConsoleWrapper = ({
saveEditorContent,
initialText,
paneKey,
height,
}:props) => {
const {
requestInFlight: requestInProgress,
lastResult: { data: requestData, error: requestError },
@ -53,26 +55,36 @@ const ConsoleWrapper = ({
const statusBarRef = useRef<HTMLDivElement>(null);
const consoleRef = useRef<HTMLDivElement>(null);
useEffect(()=>{
statusBarRef.current && consoleRef.current && (statusBarRef.current.style.width=consoleRef.current.offsetWidth+'px');
const winScroll = ()=>{
const wsTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
if(wsTop>getElementTop(consoleRef.current)) {
statusBarRef.current && (statusBarRef.current.style.position='relative');
}else{
statusBarRef.current && (statusBarRef.current.style.position='fixed');
}
}
window.addEventListener('scroll', winScroll)
return ()=>{
window.removeEventListener('scroll', winScroll)
}
},[])
// useEffect(()=>{
// const winScroll = ()=>{
// const wsTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
// if(wsTop>getElementTop(consoleRef.current)) {
// statusBarRef.current && (statusBarRef.current.style.position='relative');
// }else{
// statusBarRef.current && (statusBarRef.current.style.position='fixed');
// }
// }
// window.addEventListener('scroll', winScroll, {passive:true})
// return ()=>{
// window.removeEventListener('scroll', winScroll)
// }
// },[])
useEventListener('resize', ()=>{
statusBarRef.current && consoleRef.current && (statusBarRef.current.style.width=consoleRef.current.offsetWidth+'px');
})
useLayoutEffect(()=>{
// console.log(consoleRef.current?.offsetWidth)
if(consoleRef.current.offsetWidth>0)
statusBarRef.current && consoleRef.current && (statusBarRef.current.style.width=consoleRef.current.offsetWidth+'px');
}, [consoleRef.current?.offsetWidth])
const calcHeight = height > 0 ? (height-35)+'px' : '100%';
return (
<div>
<div ref={consoleRef} className="Console">
<div style={{height: calcHeight}}>
<div ref={consoleRef} className="Console" style={{height:'100%'}}>
<PanelsContainer resizerClassName="resizer">
<Panel style={{ height: '100%', position: 'relative', minWidth: PANEL_MIN_WIDTH }} initialWidth={INITIAL_PANEL_WIDTH}>
<ConsoleInput clusterID={selectedCluster.id} saveEditorContent={saveEditorContent} initialText={initialText} paneKey={paneKey} />
@ -82,12 +94,8 @@ const ConsoleWrapper = ({
</Panel>
</PanelsContainer>
</div>
<div ref={statusBarRef} style={{ position:'fixed', bottom:0, borderTop: '1px solid #eee', zIndex:5000}}>
<EuiFlexGroup className="consoleContainer"
style={{height:30, background:'#fff'}}
gutterSize="none"
direction="column">
<EuiFlexItem className="conApp__tabsExtension">
<div ref={statusBarRef} style={{ position:'fixed', bottom:0, borderTop: '1px solid #eee', zIndex:1001, width:'100%'}}>
<div style={{background:'#fff',height:30, width:'100%'}}>
<RequestStatusBar
requestInProgress={requestInProgress}
selectedCluster={selectedCluster}
@ -99,18 +107,20 @@ const ConsoleWrapper = ({
statusCode: lastDatum.response.statusCode,
statusText: lastDatum.response.statusText,
timeElapsedMs: lastDatum.response.timeMs,
requestHeader: lastDatum.request.header,
responseHeader: lastDatum.response.header,
}
: undefined
}
/>
</EuiFlexItem>
</EuiFlexGroup>
</div>
</div>
</div>
);
};
const Console = (params:props) => {
const registryRef = useRef(new PanelRegistry());
// const [consoleInputKey] = useMemo(()=>{
// return [selectedCluster.id + '-console-input'];

View File

@ -11,7 +11,7 @@ import './ConsoleInput.scss';
import { useSendCurrentRequestToES } from '../hooks/use_send_current_request_to_es';
import { useSetInputEditor } from '../hooks/use_set_input_editor';
import '@elastic/eui/dist/eui_theme_light.css';
import { instance as registry, editorList } from '../contexts/editor_context/editor_registry';
import { instance as registry } from '../contexts/editor_context/editor_registry';
import 'antd/dist/antd.css';
import {retrieveAutoCompleteInfo} from '../modules/mappings/mappings';
import {useSaveCurrentTextObject} from '../hooks/use_save_current_text_object';
@ -109,7 +109,6 @@ const ConsoleInputUI = ({clusterID, initialText, saveEditorContent, paneKey}:Con
editorInstanceRef.current = senseEditor;
setInputEditor(senseEditor);
senseEditor.paneKey = paneKey;
editorList.addInputEditor(senseEditor);
senseEditor.update(initialText || DEFAULT_INPUT_VALUE);
applyCurrentSettings(senseEditor!.getCoreEditor(), {fontSize:12, wrapMode: true,});
@ -151,8 +150,12 @@ const ConsoleInputUI = ({clusterID, initialText, saveEditorContent, paneKey}:Con
aceEditorRef.current && (aceEditorRef.current['clusterID'] = clusterID);
},[clusterID])
const handleSaveAsCommonCommand = async () => {
const editor = registry.getInputEditor();
const handleSaveAsCommonCommand = async () => {
const editor = editorInstanceRef.current;
if(editor == null){
console.warn('editor is null')
return
}
const requests = await editor.getRequestsInRange();
const formattedRequest = requests.map(request => ({
method: request.method,

View File

@ -0,0 +1,28 @@
.request-status-bar{
display: flex;
justify-content: center;
align-items: center;
height: 100%;
.bar-item{
flex: 0 0 50%;
.base-info{
display: flex;
font-size: 12px;
.info-item{
margin: 12px;
position: relative;
&.health{
padding-right: 14px;
}
}
}
.status_info{
display: flex;
font-size: 12px;
align-items: center;
.info-item{
margin: 12px;
}
}
}
}

View File

@ -18,7 +18,10 @@
*/
import React, { FunctionComponent } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiBadge, EuiText, EuiToolTip } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiBadge, EuiText, EuiToolTip,EuiCodeBlock } from '@elastic/eui';
import {HealthStatusCircle} from '@/components/infini/health_status_circle';
import './request_status_bar.scss';
import {Drawer, Tabs, Button} from 'antd';
export interface Props {
requestInProgress: boolean;
@ -37,6 +40,8 @@ export interface Props {
// The time, in milliseconds, that the last request took
timeElapsedMs: number;
responseHeader: string;
requestHeader: string;
};
}
@ -60,17 +65,103 @@ const mapStatusCodeToBadgeColor = (statusCode: number) => {
return 'danger';
};
export const RequestStatusBar: FunctionComponent<Props> = ({
// export const RequestStatusBar: FunctionComponent<Props> = ({
// requestInProgress,
// requestResult,
// selectedCluster,
// }) => {
// let content: React.ReactNode = null;
// const clusterContent = (<EuiFlexItem grow={false} style={{marginRight:'auto'}}>
// <EuiBadge style={{position:'relative', paddingLeft: 20}}>
// <i style={{marginRight:3, position:'absolute', top: 1, left:3}}><HealthStatusCircle status={selectedCluster.status}/></i>{selectedCluster.host}&nbsp;-&nbsp;{selectedCluster.version}
// </EuiBadge>
// </EuiFlexItem>);
// if (requestInProgress) {
// content = (
// <EuiFlexItem grow={false}>
// <EuiBadge color="hollow">
// Request in progress
// </EuiBadge>
// </EuiFlexItem>
// );
// } else if (requestResult) {
// const { endpoint, method, statusCode, statusText, timeElapsedMs } = requestResult;
// content = (
// <>
// <EuiFlexItem grow={false}>
// <EuiToolTip
// position="top"
// content={
// <EuiText size="s">{`${method} ${
// endpoint.startsWith('/') ? endpoint : '/' + endpoint
// }`}</EuiText>
// }
// >
// <EuiBadge color={mapStatusCodeToBadgeColor(statusCode)}>
// {/* Use &nbsp; to ensure that no matter the width we don't allow line breaks */}
// {statusCode}&nbsp;-&nbsp;{statusText}
// </EuiBadge>
// </EuiToolTip>
// </EuiFlexItem>
// <EuiFlexItem grow={false}>
// <EuiToolTip
// position="top"
// content={
// <EuiText size="s">
// Time Elapsed
// </EuiText>
// }
// >
// <EuiText size="s">
// <EuiBadge color="default">
// {timeElapsedMs}&nbsp;{'ms'}
// </EuiBadge>
// </EuiText>
// </EuiToolTip>
// </EuiFlexItem>
// </>
// );
// }
// return (
// <EuiFlexGroup
// justifyContent="flexEnd"
// alignItems="center"
// direction="row"
// gutterSize="s"
// responsive={false}
// >
// {clusterContent}
// {content}
// </EuiFlexGroup>
// );
// };
export const RequestStatusBar = ({
requestInProgress,
requestResult,
selectedCluster,
}) => {
}:Props) => {
let content: React.ReactNode = null;
const clusterContent = (<EuiFlexItem grow={false} style={{marginRight:'auto'}}>
<EuiBadge>
{selectedCluster.host}&nbsp;-&nbsp;{selectedCluster.version}
</EuiBadge>
</EuiFlexItem>);
const clusterContent = (<div className="base-info">
<div className="info-item health">
<span></span>
<i style={{position:'absolute', top: 1, right:0}}>
<HealthStatusCircle status={selectedCluster.status}/>
</i>
</div>
<div className="info-item">
<span></span>
{selectedCluster.host}
</div>
<div className="info-item">
<span></span>
{selectedCluster.version}
</div>
</div>);
const [headerInfoVisible, setHeaderInfoVisible] = React.useState(false)
if (requestInProgress) {
content = (
@ -85,7 +176,8 @@ export const RequestStatusBar: FunctionComponent<Props> = ({
content = (
<>
<EuiFlexItem grow={false}>
<div className="status_info">
<div className="info-item">
<EuiToolTip
position="top"
content={
@ -99,8 +191,8 @@ export const RequestStatusBar: FunctionComponent<Props> = ({
{statusCode}&nbsp;-&nbsp;{statusText}
</EuiBadge>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
</div>
<div className="info-item">
<EuiToolTip
position="top"
content={
@ -115,21 +207,48 @@ export const RequestStatusBar: FunctionComponent<Props> = ({
</EuiBadge>
</EuiText>
</EuiToolTip>
</EuiFlexItem>
</div>
<div className="info-item">
<EuiText size="s">
<Button type="link" onClick={()=>{setHeaderInfoVisible(true)}}>
Headers
</Button>
</EuiText>
</div>
</div>
</>
);
}
return (
<EuiFlexGroup
justifyContent="flexEnd"
alignItems="center"
direction="row"
gutterSize="s"
responsive={false}
>
{clusterContent}
{content}
</EuiFlexGroup>
<div className="request-status-bar">
<div className="bar-item">{clusterContent}</div>
<div className="bar-item">{content}</div>
<Drawer title="Request header info"
style={{zIndex:1004}}
width={520}
destroyOnClose={true}
visible={headerInfoVisible}
onClose={()=>{setHeaderInfoVisible(false)}}
>
<Tabs>
<Tabs.TabPane tab="Request" key="1">
<div>
<EuiCodeBlock language="text" isCopyable paddingSize="s">
{requestResult?.requestHeader}
</EuiCodeBlock>
</div>
</Tabs.TabPane>
<Tabs.TabPane tab="Response" key="2">
<EuiCodeBlock language="text" isCopyable paddingSize="s">
{requestResult?.responseHeader}
</EuiCodeBlock>
</Tabs.TabPane>
</Tabs>
</Drawer>
</div>
);
};

View File

@ -37,7 +37,6 @@ export class EditorRegistry {
setInputEditor(inputEditor: SenseEditor) {
this.inputEditor = inputEditor;
inputEditor.setAutocompleter();
}
getInputEditor() {

View File

@ -1,5 +1,6 @@
import { TokensProvider } from './tokens_provider';
import { Token } from './token';
import RowParser from './row_parser';
type MarkerRef = any;
@ -81,6 +82,9 @@ export enum LINE_MODE {
* being used which is usually vendor code such as Ace or Monaco.
*/
export interface CoreEditor {
getParser(): RowParser;
getAutocompleter(): AutoCompleterFunction;
/**
* Get the current position of the cursor.
*/

View File

@ -12,16 +12,16 @@ export class SenseEditor {
currentReqRange: (Range & { markerRef: unknown }) | null;
parser: RowParser;
private readonly autocomplete: ReturnType<typeof createAutocompleter>;
// private readonly autocomplete: ReturnType<typeof createAutocompleter>;
constructor(private readonly coreEditor: CoreEditor) {
this.currentReqRange = null;
this.parser = new RowParser(this.coreEditor);
this.autocomplete = createAutocompleter({
coreEditor,
parser: this.parser,
});
this.coreEditor.registerAutocompleter(this.autocomplete.getCompletions);
// this.parser = new RowParser(this.coreEditor);
// this.autocomplete = createAutocompleter({
// coreEditor,
// });
// this.coreEditor.registerAutocompleter(this.autocomplete.getCompletions);
this.parser = coreEditor.getParser();
this.coreEditor.on(
'tokenizerUpdate',
this.highlightCurrentRequestsAndUpdateActionBar.bind(this)
@ -30,10 +30,6 @@ export class SenseEditor {
this.coreEditor.on('changeScrollTop', this.updateActionsBar.bind(this));
}
setAutocompleter = ()=>{
this.coreEditor.registerAutocompleter(this.autocomplete.getCompletions);
}
prevRequestStart = (rowOrPos?: number | Position): Position => {
let curRow: number;

View File

@ -36,6 +36,7 @@ import { instance as registry } from '../../contexts/editor_context/editor_regis
import { useRequestActionContext } from '../../contexts/request_context';
import { useServicesContext } from '../../contexts/services_context';
import {getCommand} from '../../modules/mappings/mappings';
import {useEditorReadContext} from '../../contexts/editor_context';
function buildRawCommonCommandRequest(cmd:any){
const {requests} = cmd._source;
@ -48,10 +49,12 @@ function buildRawCommonCommandRequest(cmd:any){
export const useSendCurrentRequestToES = () => {
const dispatch = useRequestActionContext();
const { services: { history }, clusterID } = useServicesContext();
const {sensorEditor:editor} = useEditorReadContext();
return useCallback(async () => {
try {
const editor = registry.getInputEditor();
// const editor = registry.getInputEditor();
if(!editor) return
const requests = await editor.getRequestsInRange();
if (!requests.length) {
console.log('No request selected. Select a request by placing the cursor inside it.');
@ -118,5 +121,5 @@ export const useSendCurrentRequestToES = () => {
});
}
}
}, [dispatch, history, clusterID]);
}, [dispatch, history, clusterID, editor]);
};

View File

@ -74,7 +74,6 @@ export function sendRequestToES(args: EsRequestArgs): Promise<ESRequestResult[]>
if (reqId !== CURRENT_REQ_ID) {
return;
}
const xhr = dataOrjqXHR.promise ? dataOrjqXHR : jqXhrORerrorThrown;
const isSuccess =
@ -83,7 +82,10 @@ export function sendRequestToES(args: EsRequestArgs): Promise<ESRequestResult[]>
((xhr.status >= 200 && xhr.status < 300) || xhr.status === 404);
if (isSuccess) {
let value = xhr.responseText;
// let value = xhr.responseText;
let resObj = JSON.parse(xhr.responseText)
let value = resObj.response_body;
const warnings = xhr.getResponseHeader('warning');
if (warnings) {
@ -102,11 +104,13 @@ export function sendRequestToES(args: EsRequestArgs): Promise<ESRequestResult[]>
statusText: xhr.statusText,
contentType: xhr.getResponseHeader('Content-Type'),
value,
header: resObj.response_header,
},
request: {
data: esData,
method: esMethod,
path: esPath,
header: resObj.request_header,
},
});
@ -116,8 +120,15 @@ export function sendRequestToES(args: EsRequestArgs): Promise<ESRequestResult[]>
let value;
let contentType: string;
if (xhr.responseText) {
value = xhr.responseText; // ES error should be shown
contentType = xhr.getResponseHeader('Content-Type');
const resObj = JSON.parse(xhr.responseText)
if(resObj.error){
value = resObj.error;
contentType = 'text/plain';
}else{
value = resObj.response_body; // ES error should be shown
contentType = xhr.getResponseHeader('Content-Type');
}
} else {
value = 'Request failed to get to the server (status code: ' + xhr.status + ')';
contentType = 'text/plain';

View File

@ -41,7 +41,7 @@ export const useSetInputEditor = () => {
return useCallback(
(editor: SenseEditor) => {
dispatch({ type: 'setInputEditor', payload: editor });
registry.setInputEditor(editor);
// registry.setInputEditor(editor);
},
[dispatch]
);

View File

@ -360,13 +360,10 @@ function addMetaToTermsList(
// eslint-disable-next-line
export default function ({
coreEditor: editor,
parser,
}: {
coreEditor: CoreEditor;
parser: RowParser;
}) {
const parser = new RowParser(editor)
function applyTerm(term: {
value?: string;
context?: AutoCompleteContext;

View File

@ -34,6 +34,7 @@
import $ from 'jquery';
// @ts-ignore
import { stringify } from 'query-string';
import {pathPrefix} from '@/services/common';
interface SendOptions {
asSystemRequest?: boolean;
@ -103,12 +104,7 @@ export function send(
}
export function queryCommonCommands(title?: string) {
const clusterID = extractClusterIDFromURL();
if(!clusterID){
console.log('can not get clusterid from url');
return;
}
let url = `/elasticsearch/${clusterID}/command/_search`;
let url = `${pathPrefix}/elasticsearch/command`;
if(title){
url +=`?title=${title}`
}
@ -124,12 +120,7 @@ export function constructESUrl(baseUri: string, path: string) {
}
export function saveCommonCommand(params: any) {
const clusterID = extractClusterIDFromURL();
if(!clusterID){
console.log('can not get clusterid from url');
return;
}
return fetch(`/elasticsearch/${clusterID}/command`, {
return fetch(`${pathPrefix}/elasticsearch/command`, {
method: 'POST',
body: JSON.stringify(params),
headers:{

View File

@ -10,28 +10,58 @@ import { EditorEvent, AutoCompleterFunction } from '../../entities/core_editor';
import { AceTokensProvider } from '../../entities/ace_tokens_providers';
import * as curl from './curl';
import smartResize from './smart_resize';
ace.define(
'ace/autocomplete/text_completer',
['require', 'exports', 'module'],
function (
require: unknown,
exports: {
getCompletions: (
innerEditor: unknown,
session: unknown,
pos: unknown,
prefix: unknown,
callback: (e: null | Error, values: string[]) => void
) => void;
}
) {
exports.getCompletions = function (innerEditor, session, pos, prefix, callback) {
callback(null, []);
};
}
);
import createAutocompleter from '../../modules/autocomplete/autocomplete';
import RowParser from '../../entities/row_parser';
const langTools = ace.acequire('ace/ext/language_tools');
(function initAceEditor() {
ace.define(
'ace/autocomplete/text_completer',
['require', 'exports', 'module'],
function (
require: unknown,
exports: {
getCompletions: (
innerEditor: unknown,
session: unknown,
pos: unknown,
prefix: unknown,
callback: (e: null | Error, values: string[]) => void
) => void;
}
) {
exports.getCompletions = function (innerEditor, session, pos, prefix, callback) {
callback(null, []);
};
}
);
const langTools = ace.acequire('ace/ext/language_tools');
langTools.setCompleters( //addCompleters
[{
identifierRegexps: [
/[a-zA-Z_0-9\.\$\-\u00A2-\uFFFF]/, // adds support for dot character
],
getCompletions: (
// eslint-disable-next-line @typescript-eslint/naming-convention
DO_NOT_USE_1: IAceEditor,
// eslint-disable-next-line @typescript-eslint/naming-convention
DO_NOT_USE_2: IAceEditSession,
pos: { row: number; column: number },
prefix: string,
callback: (...args: unknown[]) => void
) => {
const {coreEditor} = DO_NOT_USE_1;
const position: Position = {
lineNumber: pos.row + 1,
column: pos.column + 1,
};
coreEditor.autocompleter(position, prefix, callback);
// autocompleter(position, prefix, callback);
},
}],
);
})();
// @ts-ignore
import * as InputMode from './mode/input';
@ -45,10 +75,16 @@ export class LegacyCoreEditor implements CoreEditor {
// @ts-ignore
$actions: JQuery<HTMLElement>;
resize: () => void;
private autocompleter: AutoCompleterFunction;
private parser: RowParser;
constructor(private readonly editor: IAceEditor, actions: HTMLElement) {
constructor(private editor: IAceEditor, actions: HTMLElement) {
this.$actions = $(actions);
this.editor.setShowPrintMargin(false);
this.parser = new RowParser(this);
this.autocompleter = createAutocompleter({
coreEditor: this,
}).getCompletions
const session = this.editor.getSession();
// @ts-ignore
@ -72,6 +108,14 @@ export class LegacyCoreEditor implements CoreEditor {
this.editor.$blockScrolling = Infinity;
this.hideActionsBar();
this.editor.focus();
editor.coreEditor = this;
}
getParser(): RowParser {
return this.parser;
}
getAutocompleter(): AutoCompleterFunction {
return this.autocompleter;
}
// dirty check for tokenizer state, uses a lot less cycles
@ -388,6 +432,7 @@ export class LegacyCoreEditor implements CoreEditor {
prefix: string,
callback: (...args: unknown[]) => void
) => {
debugger
const position: Position = {
lineNumber: pos.row + 1,
column: pos.column + 1,

View File

@ -223,15 +223,23 @@ function getFieldNamesFromProperties(properties = {}) {
}
function loadTemplates(templatesObject = {}, clusterID) {
templatesObject = getRawBody(templatesObject);
templates[clusterID] = Object.keys(templatesObject);
}
function getRawBody(body) {
if(body.response_body){
return JSON.parse(body.response_body);
}
return body;
}
export function loadMappings(mappings, clusterID) {
mappings = getRawBody(mappings)
let clusterPerIndexTypes = {};
$.each(mappings, function (index, indexMapping) {
const normalizedIndexMappings = {};
// Migrate 1.0.0 mappings. This format has changed, so we need to extract the underlying mapping.
if (indexMapping.mappings && _.keys(indexMapping).length === 1) {
indexMapping = indexMapping.mappings;
@ -242,7 +250,7 @@ export function loadMappings(mappings, clusterID) {
const fieldList = getFieldNamesFromProperties(typeMapping);
normalizedIndexMappings[typeName] = fieldList;
} else {
normalizedIndexMappings[typeName] = [];
normalizedIndexMappings[typeName] = getFieldNamesFromProperties(typeMapping.properties); // for es 2.x, 5.x, 6.x
}
});
clusterPerIndexTypes[index] = normalizedIndexMappings;
@ -251,6 +259,7 @@ export function loadMappings(mappings, clusterID) {
}
export function loadAliases(aliases, clusterID) {
aliases = getRawBody(aliases)
let clusterPerAliasIndexes = {};
$.each(aliases || {}, function (index, omdexAliases) {
// verify we have an index defined. useful when mapping loading is disabled

View File

@ -39,12 +39,14 @@ import { SenseEditor } from '../entities/sense_editor';
export interface Store {
ready: boolean;
currentTextObject: TextObject | null;
sensorEditor: SenseEditor | null;
}
export const initialValue: Store = produce<Store>(
{
ready: false,
currentTextObject: null,
sensorEditor: null,
},
identity
);
@ -58,7 +60,8 @@ export const reducer: Reducer<Store, Action> = (state, action) =>
if (action.type === 'setInputEditor') {
if (action.payload) {
draft.ready = true;
}
draft.sensorEditor = action.payload;
}
return;
}

View File

@ -16,7 +16,7 @@ function useStorage(key, defaultValue, storageObject, encryptor) {
return storeValue
}
if (typeof initialValue === "function") {
if (typeof defaultValue === "function") {
return defaultValue()
} else {
return defaultValue

View File

@ -0,0 +1,21 @@
import { useEffect, useRef } from "react"
export default function useEventListener(
eventType,
callback,
element = window
) {
const callbackRef = useRef(callback)
useEffect(() => {
callbackRef.current = callback
}, [callback])
useEffect(() => {
if (element == null) return
const handler = e => callbackRef.current(e)
element.addEventListener(eventType, handler)
return () => element.removeEventListener(eventType, handler)
}, [eventType, element])
}

View File

@ -0,0 +1,30 @@
import { useCallback, useEffect, useRef } from "react"
export default function useTimeout(callback, delay) {
const callbackRef = useRef(callback)
const timeoutRef = useRef()
useEffect(() => {
callbackRef.current = callback
}, [callback])
const set = useCallback(() => {
timeoutRef.current = setTimeout(() => callbackRef.current(), delay)
}, [delay])
const clear = useCallback(() => {
timeoutRef.current && clearTimeout(timeoutRef.current)
}, [])
useEffect(() => {
set()
return clear
}, [delay, set, clear])
const reset = useCallback(() => {
clear()
set()
}, [clear, set])
return { reset, clear }
}

View File

@ -4,6 +4,7 @@ import {searchClusterConfig, getClusterStatus} from "@/services/cluster";
import {formatESSearchResult, extractClusterIDFromURL} from '@/lib/elasticsearch/util';
import {Modal} from 'antd';
import router from "umi/router";
import _ from 'lodash';
const MENU_COLLAPSED_KEY = "search-center:menu:collapsed";
@ -153,21 +154,24 @@ export default {
}
}
},
*fetchClusterStatus({payload}, {call, put}){
*fetchClusterStatus({payload}, {call, put, select}){
let res = yield call(getClusterStatus, payload);
if(!res){
return false
}
const {clusterStatus} = yield select(state=>state.global);
if(res.error){
console.log(res.error)
return false;
}
yield put({
type: 'saveData',
payload: {
clusterStatus: res
}
});
if(!_.isEqual(res, clusterStatus)){
yield put({
type: 'saveData',
payload: {
clusterStatus: res
}
});
}
return res;
},
},
@ -251,7 +255,7 @@ export default {
// Subscribe history(url) change, trigger `load` action if pathname is `/`
return history.listen(({ pathname, search }) => {
let clusterVisible = true;
const clusterHiddenPath = ["/system", "/cluster/overview", "/alerting/overview", "/alerting/monitor/monitors/", "/alerting/destination"];
const clusterHiddenPath = ["/system", "/cluster/overview", "/alerting/overview", "/alerting/monitor/monitors/", "/alerting/destination", '/dev_tool'];
if(clusterHiddenPath.some(p=>pathname.startsWith(p))){
clusterVisible = false;
if(pathname.includes("elasticsearch")){

View File

@ -1,10 +1,13 @@
import Console from '../../components/kibana/console/components/Console';
import {connect} from 'dva';
import {Tabs, Button, Icon, Menu, Dropdown} from 'antd';
import {useState, useReducer, useCallback, useEffect, useMemo} from 'react';
import {useState, useReducer, useCallback, useEffect, useMemo, useRef, useLayoutEffect} from 'react';
import {useLocalStorage} from '@/lib/hooks/storage';
import {setClusterID} from '../../components/kibana/console/modules/mappings/mappings';
import {editorList} from '@/components/kibana/console/contexts/editor_context/editor_registry';
import {TabTitle} from './console_tab_title';
import '@/assets/utility.scss';
import { Resizable } from "re-resizable";
import {ResizeBar} from '@/components/infini/resize_bar';
const { TabPane } = Tabs;
@ -25,7 +28,7 @@ const addTab = (state: any, action: any) => {
const { panes } = state;
const {cluster} = action.payload;
const activeKey = `${cluster.id}:${new Date().valueOf()}`;
panes.push({ key: activeKey, cluster_id: cluster.id});
panes.push({ key: activeKey, cluster_id: cluster.id, title: cluster.name});
return {
...state,
panes,
@ -58,7 +61,22 @@ const consoleTabReducer = (state: any, action: any) => {
...state,
activeKey: payload.activeKey,
}
editorList.setActiveEditor(payload.activeKey);
break;
case 'saveTitle':
const {key, title} = action.payload;
const newPanes = state.panes.map((pane: any)=>{
if(pane.key == key){
return {
...pane,
title,
}
}
return pane;
});
newState = {
...state,
panes: newPanes,
}
break;
case 'saveContent':
const panes = state.panes.map((pane)=>{
@ -81,24 +99,59 @@ const consoleTabReducer = (state: any, action: any) => {
return newState;
}
const ConsoleUI = ({clusterList}: any)=>{
function calcHeightToPX(height: string){
const intHeight = parseInt(height)
if(height.endsWith('vh')){
return Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0) * intHeight / 100;
}else{
return intHeight;
}
}
export const ConsoleUI = ({selectedCluster,
clusterList,
clusterStatus,
minimize=false,
onMinimizeClick,
resizeable=false,
height='50vh'
}: any)=>{
const clusterMap = useMemo(()=>{
let cm = {};
if(!clusterStatus){
return cm;
}
(clusterList || []).map((cluster: any)=>{
cm[cluster.id] = cluster;
cluster.status = clusterStatus[cluster.id].health?.status;
if(!clusterStatus[cluster.id].available){
cluster.status = 'unavailable';
}
cm[cluster.id] = cluster;
});
return cm;
}, [clusterList])
const [localState, setLocalState, removeLocalState] = useLocalStorage("console:state", {
panes: [],
activeKey: '',
},{
}, [clusterList, clusterStatus])
const initialDefaultState = ()=>{
const defaultActiveKey = `${selectedCluster.id}:${new Date().valueOf()}`;
const defaultState = selectedCluster? {
panes:[{
key: defaultActiveKey, cluster_id: selectedCluster.id, title: selectedCluster.name
}],
activeKey: defaultActiveKey,
}: {panes:[],activeKey:''};
return defaultState
}
const [localState, setLocalState, removeLocalState] = useLocalStorage("console:state", initialDefaultState, {
encode: JSON.stringify,
decode: JSON.parse,
})
const [tabState, dispatch] = useReducer(consoleTabReducer, localState)
useEffect(()=>{
if(tabState.panes.length == 0){
removeLocalState()
return
}
setLocalState(tabState)
}, [tabState])
@ -150,20 +203,96 @@ const ConsoleUI = ({clusterList}: any)=>{
})}
</Menu>
);
const rootRef = useRef(null);
const [isFullscreen, setIsFullscreen] = useState(false);
const fullscreenClick = ()=>{
if(rootRef.current != null){
if(!isFullscreen){
rootRef.current.className = rootRef.current.className + " fullscreen";
// rootRef.current.style.overflow = 'scroll';
}else{
rootRef.current.className = rootRef.current.className.replace(' fullscreen', '');
}
}
setEditorHeight(rootRef.current.clientHeight)
setIsFullscreen(!isFullscreen)
}
const tabBarExtra =(<Dropdown overlay={menu}>
<Button size="small">
<Icon type="plus"/>
const tabBarExtra =(
<div>
<Dropdown overlay={menu}>
<Button size="small" style={{marginRight:5}}>
<Icon type="plus"/>
</Button>
</Dropdown>);
</Dropdown>
{isFullscreen?
<Button size="small" onClick={fullscreenClick}>
<Icon type="fullscreen-exit"/>
</Button>:
<Button size="small" onClick={fullscreenClick}>
<Icon type="fullscreen"/>
</Button>
}
{minimize? <Button size="small" onClick={onMinimizeClick} style={{marginLeft:5}}>
<Icon type="minus"/>
</Button>:null}
</div>
);
setClusterID(tabState.activeKey?.split(':')[0]);
const panes = tabState.panes.filter((pane: any)=>{
return typeof clusterMap[pane.cluster_id] != 'undefined';
})
const saveTitle = (key: string, title: string)=>{
dispatch({
type:'saveTitle',
payload: {
key,
title,
}
})
}
const [editorHeight, setEditorHeight] = useState(calcHeightToPX(height))
const onResize = (_env, _dir, refToElement, delta)=>{
// console.log(refToElement.offsetHeight, delta)
setEditorHeight(refToElement.clientHeight)
}
const disableWindowScroll = ()=>{
document.body.style.overflow = 'hidden'
}
const enableWindowScroll = ()=>{
document.body.style.overflow = '';
}
return (
<div style={{background:'#fff'}}>
<Resizable
defaultSize={{
height: editorHeight||'50vh'
}}
minHeight={200}
maxHeight="100vh"
handleComponent={{ top: <ResizeBar/> }}
onResize={onResize}
enable={{
top: resizeable,
right: false,
bottom: false,
left: false,
topRight: false,
bottomRight: false,
bottomLeft: false,
topLeft: false,
}}>
<div style={{background:'#fff', height:'100%'}}
onMouseOver={disableWindowScroll}
onMouseOut={enableWindowScroll}
id="console"
ref={rootRef} >
<Tabs
onChange={onChange}
activeKey={tabState.activeKey}
@ -173,13 +302,14 @@ const ConsoleUI = ({clusterList}: any)=>{
tabBarExtraContent={tabBarExtra}
>
{panes.map(pane => (
<TabPane tab={clusterMap[pane.cluster_id].name} key={pane.key} closable={pane.closable}>
<TabConsole selectedCluster={clusterMap[pane.cluster_id]} paneKey={pane.key} saveEditorContent={saveEditorContent} initialText={pane.content} />
<TabPane tab={<TabTitle title={pane.title} onTitleChange={(title)=>{saveTitle(pane.key, title)}}/>} key={pane.key} closable={pane.closable}>
<TabConsole height={editorHeight - 40} selectedCluster={clusterMap[pane.cluster_id]} paneKey={pane.key} saveEditorContent={saveEditorContent} initialText={pane.content} />
{/* {pane.content} */}
</TabPane>
))}
</Tabs>
</div>
</Resizable>
);
}
@ -188,4 +318,6 @@ export default connect(({
})=>({
selectedCluster: global.selectedCluster,
clusterList: global.clusterList,
clusterStatus: global.clusterStatus,
height: window.innerHeight - 75 + 'px',
}))(ConsoleUI);

View File

@ -0,0 +1,19 @@
.tab-title{
display: inline-block;
.input-eidtor{
border-radius: 0;
border:none;
border-bottom: 1px solid #ccc;
border-right: none;
padding-left: 1em;
&:focus{
outline: none;
}
}
}
#console{
.ant-tabs-bar{
margin: 0px 0 5px 0;
}
}

View File

@ -0,0 +1,34 @@
import {useState, useRef, useEffect} from 'react';
import './console_tab_title.scss';
interface TabTitleProps {
title: string,
onTitleChange?: (title: string)=>void;
}
export const TabTitle = ({title, onTitleChange}: TabTitleProps)=>{
const [editable, setEditable] = useState(false);
const [value, setValue] = useState(title);
const onValueChange = (e: any)=>{
const newVal = e.target.value;
setValue(newVal);
if(typeof onTitleChange == 'function') onTitleChange(newVal);
}
useEffect(()=>{
if(editable){
inputRef.current?.focus();
}
},[editable])
const inputRef = useRef(null);
return (<div title="double click to change title" className="tab-title" onDoubleClick={()=>{
setEditable(true)
}}>
{editable ? <input ref={inputRef} className="input-eidtor"
type="text" value={value}
onBlur={()=>{
setEditable(false)
}}
onChange={onValueChange}/>:value}
</div>)
}