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.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(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 //new api
ui.HandleUIMethod(api.GET, path.Join(pathPrefix, "alerting/overview"), alerting.GetAlertOverview) ui.HandleUIMethod(api.GET, path.Join(pathPrefix, "alerting/overview"), alerting.GetAlertOverview)
ui.HandleUIMethod(api.GET, path.Join(pathPrefix, "alerting/overview/alerts"), alerting.GetAlerts) 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.GET, "/elasticsearch/:id/alerting/alerts", alerting.GetAlerts)
ui.HandleUIMethod(api.POST, "/elasticsearch/:id/alerting/_monitors/:monitorID/_acknowledge/alerts", alerting.AcknowledgeAlerts) ui.HandleUIMethod(api.POST, "/elasticsearch/:id/alerting/_monitors/:monitorID/_acknowledge/alerts", alerting.AcknowledgeAlerts)
task.RegisterScheduleTask(task.ScheduleTask{ task.RegisterScheduleTask(task.ScheduleTask{
Description: "sync reindex task result", Description: "sync reindex task result",
Task: func() { Task: func() {

View File

@ -1,8 +1,8 @@
package config package config
const LastCommitLog = "N/A" const LastCommitLog = "b8fb6a3, Fri Oct 15 11:41:38 2021 +0800, liugq, console tab v0.1 "
const BuildDate = "N/A" 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 HeaderSearch from '../HeaderSearch';
import SelectLang from '../SelectLang'; import SelectLang from '../SelectLang';
import styles from './index.less'; 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 { export default class GlobalHeaderRight extends PureComponent {
state={consoleVisible: false}
getNoticeData() { getNoticeData() {
const { notices = [] } = this.props; const { notices = [] } = this.props;
if (notices.length === 0) { if (notices.length === 0) {
@ -95,7 +99,10 @@ export default class GlobalHeaderRight extends PureComponent {
<a className={styles.action} onClick={()=>{ <a className={styles.action} onClick={()=>{
const {history, selectedCluster} = this.props; 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> }}> <Icon type="code" /></a>
{/* <NoticeIcon {/* <NoticeIcon
@ -151,7 +158,52 @@ export default class GlobalHeaderRight extends PureComponent {
<Spin size="small" style={{ marginLeft: 8, marginRight: 8 }} /> <Spin size="small" style={{ marginLeft: 8, marginRight: 8 }} />
)} */} )} */}
<SelectLang className={styles.action} /> <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> </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> = { const statusColorMap: Record<string, string> = {
'green': '#39b362', 'green': '#39b362',
@ -15,6 +16,9 @@ interface props {
} }
export const HealthStatusCircle = ({status}: 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); const color = convertStatusToColor(status);
return <div style={{background: color, height:14, width:14, borderRadius: 14, boxShadow: '0px 0px 5px #999', display: 'inline-block'}}></div> 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 ( 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 layout="vertical">
<Form.Item label="标题"> <Form.Item label="标题">
{form.getFieldDecorator('title', { {form.getFieldDecorator('title', {

View File

@ -1,5 +1,5 @@
// @ts-ignore // @ts-ignore
import React, { useRef, useMemo,useEffect } from 'react'; import React, { useRef, useMemo,useEffect, useLayoutEffect } from 'react';
import ConsoleInput from './ConsoleInput'; import ConsoleInput from './ConsoleInput';
import ConsoleOutput from './ConsoleOutput'; import ConsoleOutput from './ConsoleOutput';
import { Panel } from './Panel'; import { Panel } from './Panel';
@ -14,12 +14,14 @@ import { createHistory, History, createStorage, createSettings } from '../servic
import { create } from '../storage/local_storage_object_client'; import { create } from '../storage/local_storage_object_client';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import {RequestStatusBar} from './request_status_bar'; import {RequestStatusBar} from './request_status_bar';
import useEventListener from '@/lib/hooks/use_event_listener';
interface props { interface props {
selectedCluster: any; selectedCluster: any;
saveEditorContent: (content: string)=>void; saveEditorContent: (content: string)=>void;
initialText: string; initialText: string;
paneKey: string; paneKey: string;
height: number;
} }
const INITIAL_PANEL_WIDTH = 50; const INITIAL_PANEL_WIDTH = 50;
@ -30,8 +32,8 @@ const ConsoleWrapper = ({
saveEditorContent, saveEditorContent,
initialText, initialText,
paneKey, paneKey,
height,
}:props) => { }:props) => {
const { const {
requestInFlight: requestInProgress, requestInFlight: requestInProgress,
lastResult: { data: requestData, error: requestError }, lastResult: { data: requestData, error: requestError },
@ -53,26 +55,36 @@ const ConsoleWrapper = ({
const statusBarRef = useRef<HTMLDivElement>(null); const statusBarRef = useRef<HTMLDivElement>(null);
const consoleRef = useRef<HTMLDivElement>(null); const consoleRef = useRef<HTMLDivElement>(null);
useEffect(()=>{ // useEffect(()=>{
statusBarRef.current && consoleRef.current && (statusBarRef.current.style.width=consoleRef.current.offsetWidth+'px'); // const winScroll = ()=>{
const winScroll = ()=>{ // const wsTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
const wsTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop; // if(wsTop>getElementTop(consoleRef.current)) {
if(wsTop>getElementTop(consoleRef.current)) { // statusBarRef.current && (statusBarRef.current.style.position='relative');
statusBarRef.current && (statusBarRef.current.style.position='relative'); // }else{
}else{ // statusBarRef.current && (statusBarRef.current.style.position='fixed');
statusBarRef.current && (statusBarRef.current.style.position='fixed'); // }
} // }
} // window.addEventListener('scroll', winScroll, {passive:true})
window.addEventListener('scroll', winScroll) // return ()=>{
return ()=>{ // window.removeEventListener('scroll', winScroll)
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 ( return (
<div> <div style={{height: calcHeight}}>
<div ref={consoleRef} className="Console"> <div ref={consoleRef} className="Console" style={{height:'100%'}}>
<PanelsContainer resizerClassName="resizer"> <PanelsContainer resizerClassName="resizer">
<Panel style={{ height: '100%', position: 'relative', minWidth: PANEL_MIN_WIDTH }} initialWidth={INITIAL_PANEL_WIDTH}> <Panel style={{ height: '100%', position: 'relative', minWidth: PANEL_MIN_WIDTH }} initialWidth={INITIAL_PANEL_WIDTH}>
<ConsoleInput clusterID={selectedCluster.id} saveEditorContent={saveEditorContent} initialText={initialText} paneKey={paneKey} /> <ConsoleInput clusterID={selectedCluster.id} saveEditorContent={saveEditorContent} initialText={initialText} paneKey={paneKey} />
@ -82,12 +94,8 @@ const ConsoleWrapper = ({
</Panel> </Panel>
</PanelsContainer> </PanelsContainer>
</div> </div>
<div ref={statusBarRef} style={{ position:'fixed', bottom:0, borderTop: '1px solid #eee', zIndex:5000}}> <div ref={statusBarRef} style={{ position:'fixed', bottom:0, borderTop: '1px solid #eee', zIndex:1001, width:'100%'}}>
<EuiFlexGroup className="consoleContainer" <div style={{background:'#fff',height:30, width:'100%'}}>
style={{height:30, background:'#fff'}}
gutterSize="none"
direction="column">
<EuiFlexItem className="conApp__tabsExtension">
<RequestStatusBar <RequestStatusBar
requestInProgress={requestInProgress} requestInProgress={requestInProgress}
selectedCluster={selectedCluster} selectedCluster={selectedCluster}
@ -99,18 +107,20 @@ const ConsoleWrapper = ({
statusCode: lastDatum.response.statusCode, statusCode: lastDatum.response.statusCode,
statusText: lastDatum.response.statusText, statusText: lastDatum.response.statusText,
timeElapsedMs: lastDatum.response.timeMs, timeElapsedMs: lastDatum.response.timeMs,
requestHeader: lastDatum.request.header,
responseHeader: lastDatum.response.header,
} }
: undefined : undefined
} }
/> />
</EuiFlexItem> </div>
</EuiFlexGroup>
</div> </div>
</div> </div>
); );
}; };
const Console = (params:props) => { const Console = (params:props) => {
const registryRef = useRef(new PanelRegistry()); const registryRef = useRef(new PanelRegistry());
// const [consoleInputKey] = useMemo(()=>{ // const [consoleInputKey] = useMemo(()=>{
// return [selectedCluster.id + '-console-input']; // 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 { useSendCurrentRequestToES } from '../hooks/use_send_current_request_to_es';
import { useSetInputEditor } from '../hooks/use_set_input_editor'; import { useSetInputEditor } from '../hooks/use_set_input_editor';
import '@elastic/eui/dist/eui_theme_light.css'; 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 'antd/dist/antd.css';
import {retrieveAutoCompleteInfo} from '../modules/mappings/mappings'; import {retrieveAutoCompleteInfo} from '../modules/mappings/mappings';
import {useSaveCurrentTextObject} from '../hooks/use_save_current_text_object'; import {useSaveCurrentTextObject} from '../hooks/use_save_current_text_object';
@ -109,7 +109,6 @@ const ConsoleInputUI = ({clusterID, initialText, saveEditorContent, paneKey}:Con
editorInstanceRef.current = senseEditor; editorInstanceRef.current = senseEditor;
setInputEditor(senseEditor); setInputEditor(senseEditor);
senseEditor.paneKey = paneKey; senseEditor.paneKey = paneKey;
editorList.addInputEditor(senseEditor);
senseEditor.update(initialText || DEFAULT_INPUT_VALUE); senseEditor.update(initialText || DEFAULT_INPUT_VALUE);
applyCurrentSettings(senseEditor!.getCoreEditor(), {fontSize:12, wrapMode: true,}); applyCurrentSettings(senseEditor!.getCoreEditor(), {fontSize:12, wrapMode: true,});
@ -152,7 +151,11 @@ const ConsoleInputUI = ({clusterID, initialText, saveEditorContent, paneKey}:Con
},[clusterID]) },[clusterID])
const handleSaveAsCommonCommand = async () => { const handleSaveAsCommonCommand = async () => {
const editor = registry.getInputEditor(); const editor = editorInstanceRef.current;
if(editor == null){
console.warn('editor is null')
return
}
const requests = await editor.getRequestsInRange(); const requests = await editor.getRequestsInRange();
const formattedRequest = requests.map(request => ({ const formattedRequest = requests.map(request => ({
method: request.method, 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 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 { export interface Props {
requestInProgress: boolean; requestInProgress: boolean;
@ -37,6 +40,8 @@ export interface Props {
// The time, in milliseconds, that the last request took // The time, in milliseconds, that the last request took
timeElapsedMs: number; timeElapsedMs: number;
responseHeader: string;
requestHeader: string;
}; };
} }
@ -60,17 +65,103 @@ const mapStatusCodeToBadgeColor = (statusCode: number) => {
return 'danger'; 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, requestInProgress,
requestResult, requestResult,
selectedCluster, selectedCluster,
}) => { }:Props) => {
let content: React.ReactNode = null; let content: React.ReactNode = null;
const clusterContent = (<EuiFlexItem grow={false} style={{marginRight:'auto'}}> const clusterContent = (<div className="base-info">
<EuiBadge> <div className="info-item health">
{selectedCluster.host}&nbsp;-&nbsp;{selectedCluster.version} <span></span>
</EuiBadge> <i style={{position:'absolute', top: 1, right:0}}>
</EuiFlexItem>); <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) { if (requestInProgress) {
content = ( content = (
@ -85,7 +176,8 @@ export const RequestStatusBar: FunctionComponent<Props> = ({
content = ( content = (
<> <>
<EuiFlexItem grow={false}> <div className="status_info">
<div className="info-item">
<EuiToolTip <EuiToolTip
position="top" position="top"
content={ content={
@ -99,8 +191,8 @@ export const RequestStatusBar: FunctionComponent<Props> = ({
{statusCode}&nbsp;-&nbsp;{statusText} {statusCode}&nbsp;-&nbsp;{statusText}
</EuiBadge> </EuiBadge>
</EuiToolTip> </EuiToolTip>
</EuiFlexItem> </div>
<EuiFlexItem grow={false}> <div className="info-item">
<EuiToolTip <EuiToolTip
position="top" position="top"
content={ content={
@ -115,21 +207,48 @@ export const RequestStatusBar: FunctionComponent<Props> = ({
</EuiBadge> </EuiBadge>
</EuiText> </EuiText>
</EuiToolTip> </EuiToolTip>
</EuiFlexItem> </div>
<div className="info-item">
<EuiText size="s">
<Button type="link" onClick={()=>{setHeaderInfoVisible(true)}}>
Headers
</Button>
</EuiText>
</div>
</div>
</> </>
); );
} }
return ( return (
<EuiFlexGroup <div className="request-status-bar">
justifyContent="flexEnd" <div className="bar-item">{clusterContent}</div>
alignItems="center" <div className="bar-item">{content}</div>
direction="row" <Drawer title="Request header info"
gutterSize="s" style={{zIndex:1004}}
responsive={false} width={520}
destroyOnClose={true}
visible={headerInfoVisible}
onClose={()=>{setHeaderInfoVisible(false)}}
> >
{clusterContent} <Tabs>
{content} <Tabs.TabPane tab="Request" key="1">
</EuiFlexGroup> <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) { setInputEditor(inputEditor: SenseEditor) {
this.inputEditor = inputEditor; this.inputEditor = inputEditor;
inputEditor.setAutocompleter();
} }
getInputEditor() { getInputEditor() {

View File

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

View File

@ -12,16 +12,16 @@ export class SenseEditor {
currentReqRange: (Range & { markerRef: unknown }) | null; currentReqRange: (Range & { markerRef: unknown }) | null;
parser: RowParser; parser: RowParser;
private readonly autocomplete: ReturnType<typeof createAutocompleter>; // private readonly autocomplete: ReturnType<typeof createAutocompleter>;
constructor(private readonly coreEditor: CoreEditor) { constructor(private readonly coreEditor: CoreEditor) {
this.currentReqRange = null; this.currentReqRange = null;
this.parser = new RowParser(this.coreEditor); // this.parser = new RowParser(this.coreEditor);
this.autocomplete = createAutocompleter({ // this.autocomplete = createAutocompleter({
coreEditor, // coreEditor,
parser: this.parser, // });
}); // this.coreEditor.registerAutocompleter(this.autocomplete.getCompletions);
this.coreEditor.registerAutocompleter(this.autocomplete.getCompletions); this.parser = coreEditor.getParser();
this.coreEditor.on( this.coreEditor.on(
'tokenizerUpdate', 'tokenizerUpdate',
this.highlightCurrentRequestsAndUpdateActionBar.bind(this) this.highlightCurrentRequestsAndUpdateActionBar.bind(this)
@ -30,10 +30,6 @@ export class SenseEditor {
this.coreEditor.on('changeScrollTop', this.updateActionsBar.bind(this)); this.coreEditor.on('changeScrollTop', this.updateActionsBar.bind(this));
} }
setAutocompleter = ()=>{
this.coreEditor.registerAutocompleter(this.autocomplete.getCompletions);
}
prevRequestStart = (rowOrPos?: number | Position): Position => { prevRequestStart = (rowOrPos?: number | Position): Position => {
let curRow: number; 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 { useRequestActionContext } from '../../contexts/request_context';
import { useServicesContext } from '../../contexts/services_context'; import { useServicesContext } from '../../contexts/services_context';
import {getCommand} from '../../modules/mappings/mappings'; import {getCommand} from '../../modules/mappings/mappings';
import {useEditorReadContext} from '../../contexts/editor_context';
function buildRawCommonCommandRequest(cmd:any){ function buildRawCommonCommandRequest(cmd:any){
const {requests} = cmd._source; const {requests} = cmd._source;
@ -48,10 +49,12 @@ function buildRawCommonCommandRequest(cmd:any){
export const useSendCurrentRequestToES = () => { export const useSendCurrentRequestToES = () => {
const dispatch = useRequestActionContext(); const dispatch = useRequestActionContext();
const { services: { history }, clusterID } = useServicesContext(); const { services: { history }, clusterID } = useServicesContext();
const {sensorEditor:editor} = useEditorReadContext();
return useCallback(async () => { return useCallback(async () => {
try { try {
const editor = registry.getInputEditor(); // const editor = registry.getInputEditor();
if(!editor) return
const requests = await editor.getRequestsInRange(); const requests = await editor.getRequestsInRange();
if (!requests.length) { if (!requests.length) {
console.log('No request selected. Select a request by placing the cursor inside it.'); 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) { if (reqId !== CURRENT_REQ_ID) {
return; return;
} }
const xhr = dataOrjqXHR.promise ? dataOrjqXHR : jqXhrORerrorThrown; const xhr = dataOrjqXHR.promise ? dataOrjqXHR : jqXhrORerrorThrown;
const isSuccess = const isSuccess =
@ -83,7 +82,10 @@ export function sendRequestToES(args: EsRequestArgs): Promise<ESRequestResult[]>
((xhr.status >= 200 && xhr.status < 300) || xhr.status === 404); ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 404);
if (isSuccess) { 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'); const warnings = xhr.getResponseHeader('warning');
if (warnings) { if (warnings) {
@ -102,11 +104,13 @@ export function sendRequestToES(args: EsRequestArgs): Promise<ESRequestResult[]>
statusText: xhr.statusText, statusText: xhr.statusText,
contentType: xhr.getResponseHeader('Content-Type'), contentType: xhr.getResponseHeader('Content-Type'),
value, value,
header: resObj.response_header,
}, },
request: { request: {
data: esData, data: esData,
method: esMethod, method: esMethod,
path: esPath, path: esPath,
header: resObj.request_header,
}, },
}); });
@ -116,8 +120,15 @@ export function sendRequestToES(args: EsRequestArgs): Promise<ESRequestResult[]>
let value; let value;
let contentType: string; let contentType: string;
if (xhr.responseText) { if (xhr.responseText) {
value = xhr.responseText; // ES error should be shown 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'); contentType = xhr.getResponseHeader('Content-Type');
}
} else { } else {
value = 'Request failed to get to the server (status code: ' + xhr.status + ')'; value = 'Request failed to get to the server (status code: ' + xhr.status + ')';
contentType = 'text/plain'; contentType = 'text/plain';

View File

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

View File

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

View File

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

View File

@ -10,6 +10,10 @@ import { EditorEvent, AutoCompleterFunction } from '../../entities/core_editor';
import { AceTokensProvider } from '../../entities/ace_tokens_providers'; import { AceTokensProvider } from '../../entities/ace_tokens_providers';
import * as curl from './curl'; import * as curl from './curl';
import smartResize from './smart_resize'; import smartResize from './smart_resize';
import createAutocompleter from '../../modules/autocomplete/autocomplete';
import RowParser from '../../entities/row_parser';
(function initAceEditor() {
ace.define( ace.define(
'ace/autocomplete/text_completer', 'ace/autocomplete/text_completer',
['require', 'exports', 'module'], ['require', 'exports', 'module'],
@ -33,6 +37,32 @@ ace.define(
const langTools = ace.acequire('ace/ext/language_tools'); 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 // @ts-ignore
import * as InputMode from './mode/input'; import * as InputMode from './mode/input';
const _AceRange = ace.acequire('ace/range').Range; const _AceRange = ace.acequire('ace/range').Range;
@ -45,10 +75,16 @@ export class LegacyCoreEditor implements CoreEditor {
// @ts-ignore // @ts-ignore
$actions: JQuery<HTMLElement>; $actions: JQuery<HTMLElement>;
resize: () => void; 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.$actions = $(actions);
this.editor.setShowPrintMargin(false); this.editor.setShowPrintMargin(false);
this.parser = new RowParser(this);
this.autocompleter = createAutocompleter({
coreEditor: this,
}).getCompletions
const session = this.editor.getSession(); const session = this.editor.getSession();
// @ts-ignore // @ts-ignore
@ -72,6 +108,14 @@ export class LegacyCoreEditor implements CoreEditor {
this.editor.$blockScrolling = Infinity; this.editor.$blockScrolling = Infinity;
this.hideActionsBar(); this.hideActionsBar();
this.editor.focus(); 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 // dirty check for tokenizer state, uses a lot less cycles
@ -388,6 +432,7 @@ export class LegacyCoreEditor implements CoreEditor {
prefix: string, prefix: string,
callback: (...args: unknown[]) => void callback: (...args: unknown[]) => void
) => { ) => {
debugger
const position: Position = { const position: Position = {
lineNumber: pos.row + 1, lineNumber: pos.row + 1,
column: pos.column + 1, column: pos.column + 1,

View File

@ -223,15 +223,23 @@ function getFieldNamesFromProperties(properties = {}) {
} }
function loadTemplates(templatesObject = {}, clusterID) { function loadTemplates(templatesObject = {}, clusterID) {
templatesObject = getRawBody(templatesObject);
templates[clusterID] = Object.keys(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) { export function loadMappings(mappings, clusterID) {
mappings = getRawBody(mappings)
let clusterPerIndexTypes = {}; let clusterPerIndexTypes = {};
$.each(mappings, function (index, indexMapping) { $.each(mappings, function (index, indexMapping) {
const normalizedIndexMappings = {}; const normalizedIndexMappings = {};
// Migrate 1.0.0 mappings. This format has changed, so we need to extract the underlying mapping. // Migrate 1.0.0 mappings. This format has changed, so we need to extract the underlying mapping.
if (indexMapping.mappings && _.keys(indexMapping).length === 1) { if (indexMapping.mappings && _.keys(indexMapping).length === 1) {
indexMapping = indexMapping.mappings; indexMapping = indexMapping.mappings;
@ -242,7 +250,7 @@ export function loadMappings(mappings, clusterID) {
const fieldList = getFieldNamesFromProperties(typeMapping); const fieldList = getFieldNamesFromProperties(typeMapping);
normalizedIndexMappings[typeName] = fieldList; normalizedIndexMappings[typeName] = fieldList;
} else { } else {
normalizedIndexMappings[typeName] = []; normalizedIndexMappings[typeName] = getFieldNamesFromProperties(typeMapping.properties); // for es 2.x, 5.x, 6.x
} }
}); });
clusterPerIndexTypes[index] = normalizedIndexMappings; clusterPerIndexTypes[index] = normalizedIndexMappings;
@ -251,6 +259,7 @@ export function loadMappings(mappings, clusterID) {
} }
export function loadAliases(aliases, clusterID) { export function loadAliases(aliases, clusterID) {
aliases = getRawBody(aliases)
let clusterPerAliasIndexes = {}; let clusterPerAliasIndexes = {};
$.each(aliases || {}, function (index, omdexAliases) { $.each(aliases || {}, function (index, omdexAliases) {
// verify we have an index defined. useful when mapping loading is disabled // 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 { export interface Store {
ready: boolean; ready: boolean;
currentTextObject: TextObject | null; currentTextObject: TextObject | null;
sensorEditor: SenseEditor | null;
} }
export const initialValue: Store = produce<Store>( export const initialValue: Store = produce<Store>(
{ {
ready: false, ready: false,
currentTextObject: null, currentTextObject: null,
sensorEditor: null,
}, },
identity identity
); );
@ -58,6 +60,7 @@ export const reducer: Reducer<Store, Action> = (state, action) =>
if (action.type === 'setInputEditor') { if (action.type === 'setInputEditor') {
if (action.payload) { if (action.payload) {
draft.ready = true; draft.ready = true;
draft.sensorEditor = action.payload;
} }
return; return;
} }

View File

@ -16,7 +16,7 @@ function useStorage(key, defaultValue, storageObject, encryptor) {
return storeValue return storeValue
} }
if (typeof initialValue === "function") { if (typeof defaultValue === "function") {
return defaultValue() return defaultValue()
} else { } else {
return defaultValue 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 {formatESSearchResult, extractClusterIDFromURL} from '@/lib/elasticsearch/util';
import {Modal} from 'antd'; import {Modal} from 'antd';
import router from "umi/router"; import router from "umi/router";
import _ from 'lodash';
const MENU_COLLAPSED_KEY = "search-center:menu:collapsed"; 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); let res = yield call(getClusterStatus, payload);
if(!res){ if(!res){
return false return false
} }
const {clusterStatus} = yield select(state=>state.global);
if(res.error){ if(res.error){
console.log(res.error) console.log(res.error)
return false; return false;
} }
if(!_.isEqual(res, clusterStatus)){
yield put({ yield put({
type: 'saveData', type: 'saveData',
payload: { payload: {
clusterStatus: res clusterStatus: res
} }
}); });
}
return res; return res;
}, },
}, },
@ -251,7 +255,7 @@ export default {
// Subscribe history(url) change, trigger `load` action if pathname is `/` // Subscribe history(url) change, trigger `load` action if pathname is `/`
return history.listen(({ pathname, search }) => { return history.listen(({ pathname, search }) => {
let clusterVisible = true; 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))){ if(clusterHiddenPath.some(p=>pathname.startsWith(p))){
clusterVisible = false; clusterVisible = false;
if(pathname.includes("elasticsearch")){ if(pathname.includes("elasticsearch")){

View File

@ -1,10 +1,13 @@
import Console from '../../components/kibana/console/components/Console'; import Console from '../../components/kibana/console/components/Console';
import {connect} from 'dva'; import {connect} from 'dva';
import {Tabs, Button, Icon, Menu, Dropdown} from 'antd'; 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 {useLocalStorage} from '@/lib/hooks/storage';
import {setClusterID} from '../../components/kibana/console/modules/mappings/mappings'; 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; const { TabPane } = Tabs;
@ -25,7 +28,7 @@ const addTab = (state: any, action: any) => {
const { panes } = state; const { panes } = state;
const {cluster} = action.payload; const {cluster} = action.payload;
const activeKey = `${cluster.id}:${new Date().valueOf()}`; 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 { return {
...state, ...state,
panes, panes,
@ -58,7 +61,22 @@ const consoleTabReducer = (state: any, action: any) => {
...state, ...state,
activeKey: payload.activeKey, 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; break;
case 'saveContent': case 'saveContent':
const panes = state.panes.map((pane)=>{ const panes = state.panes.map((pane)=>{
@ -81,24 +99,59 @@ const consoleTabReducer = (state: any, action: any) => {
return newState; 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(()=>{ const clusterMap = useMemo(()=>{
let cm = {}; let cm = {};
if(!clusterStatus){
return cm;
}
(clusterList || []).map((cluster: any)=>{ (clusterList || []).map((cluster: any)=>{
cluster.status = clusterStatus[cluster.id].health?.status;
if(!clusterStatus[cluster.id].available){
cluster.status = 'unavailable';
}
cm[cluster.id] = cluster; cm[cluster.id] = cluster;
}); });
return cm; return cm;
}, [clusterList]) }, [clusterList, clusterStatus])
const [localState, setLocalState, removeLocalState] = useLocalStorage("console:state", { const initialDefaultState = ()=>{
panes: [], const defaultActiveKey = `${selectedCluster.id}:${new Date().valueOf()}`;
activeKey: '', 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, encode: JSON.stringify,
decode: JSON.parse, decode: JSON.parse,
}) })
const [tabState, dispatch] = useReducer(consoleTabReducer, localState) const [tabState, dispatch] = useReducer(consoleTabReducer, localState)
useEffect(()=>{ useEffect(()=>{
if(tabState.panes.length == 0){
removeLocalState()
return
}
setLocalState(tabState) setLocalState(tabState)
}, [tabState]) }, [tabState])
@ -151,19 +204,95 @@ const ConsoleUI = ({clusterList}: any)=>{
</Menu> </Menu>
); );
const tabBarExtra =(<Dropdown overlay={menu}> const rootRef = useRef(null);
<Button size="small"> 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 =(
<div>
<Dropdown overlay={menu}>
<Button size="small" style={{marginRight:5}}>
<Icon type="plus"/> <Icon type="plus"/>
</Button> </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]); setClusterID(tabState.activeKey?.split(':')[0]);
const panes = tabState.panes.filter((pane: any)=>{ const panes = tabState.panes.filter((pane: any)=>{
return typeof clusterMap[pane.cluster_id] != 'undefined'; 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 ( 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 <Tabs
onChange={onChange} onChange={onChange}
activeKey={tabState.activeKey} activeKey={tabState.activeKey}
@ -173,13 +302,14 @@ const ConsoleUI = ({clusterList}: any)=>{
tabBarExtraContent={tabBarExtra} tabBarExtraContent={tabBarExtra}
> >
{panes.map(pane => ( {panes.map(pane => (
<TabPane tab={clusterMap[pane.cluster_id].name} key={pane.key} closable={pane.closable}> <TabPane tab={<TabTitle title={pane.title} onTitleChange={(title)=>{saveTitle(pane.key, title)}}/>} key={pane.key} closable={pane.closable}>
<TabConsole selectedCluster={clusterMap[pane.cluster_id]} paneKey={pane.key} saveEditorContent={saveEditorContent} initialText={pane.content} /> <TabConsole height={editorHeight - 40} selectedCluster={clusterMap[pane.cluster_id]} paneKey={pane.key} saveEditorContent={saveEditorContent} initialText={pane.content} />
{/* {pane.content} */} {/* {pane.content} */}
</TabPane> </TabPane>
))} ))}
</Tabs> </Tabs>
</div> </div>
</Resizable>
); );
} }
@ -188,4 +318,6 @@ export default connect(({
})=>({ })=>({
selectedCluster: global.selectedCluster, selectedCluster: global.selectedCluster,
clusterList: global.clusterList, clusterList: global.clusterList,
clusterStatus: global.clusterStatus,
height: window.innerHeight - 75 + 'px',
}))(ConsoleUI); }))(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>)
}