diff --git a/api/index_management/common_command.go b/api/index_management/common_command.go new file mode 100644 index 00000000..b76bf5f9 --- /dev/null +++ b/api/index_management/common_command.go @@ -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) +} diff --git a/api/init.go b/api/init.go index 7e6ae2fa..1fb9c90e 100644 --- a/api/init.go +++ b/api/init.go @@ -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() { diff --git a/config/generated.go b/config/generated.go index 65c4273d..3b59192e 100644 --- a/config/generated.go +++ b/config/generated.go @@ -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" diff --git a/web/src/assets/utility.scss b/web/src/assets/utility.scss new file mode 100644 index 00000000..2e5a9b44 --- /dev/null +++ b/web/src/assets/utility.scss @@ -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); +} \ No newline at end of file diff --git a/web/src/components/GlobalHeader/RightContent.js b/web/src/components/GlobalHeader/RightContent.js index 154e0059..576d3117 100644 --- a/web/src/components/GlobalHeader/RightContent.js +++ b/web/src/components/GlobalHeader/RightContent.js @@ -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 { { const {history, selectedCluster} = this.props; - history.push(`/dev_tool/elasticsearch/${selectedCluster.id}/`); + // history.push(`/dev_tool`); + this.setState({ + consoleVisible: !this.state.consoleVisible + }) }}> {/* )} */} +
+ {/* }} + enable={{ + top: true, + right: false, + bottom: false, + left: false, + topRight: false, + bottomRight: false, + bottomLeft: false, + topLeft: false, + }}> */} + { + this.setState({ + consoleVisible: false, + }) + }} + clusterStatus={this.props.clusterStatus} + resizeable={true} + /> + {/* */} +
); } } + +const TopHandle = () => { + return
hello world
; +}; \ No newline at end of file diff --git a/web/src/components/infini/health_status_circle.tsx b/web/src/components/infini/health_status_circle.tsx index a8dc4bf1..497a15ce 100644 --- a/web/src/components/infini/health_status_circle.tsx +++ b/web/src/components/infini/health_status_circle.tsx @@ -1,4 +1,5 @@ -export type ClusterHealthStatus = 'green' | 'yellow' | 'red'; +import {Icon} from 'antd'; +export type ClusterHealthStatus = 'green' | 'yellow' | 'red' | 'unavailable'; const statusColorMap: Record = { 'green': '#39b362', @@ -15,6 +16,9 @@ interface props { } export const HealthStatusCircle = ({status}: props)=>{ + if(status == 'unavailable'){ + return + } const color = convertStatusToColor(status); return
} \ No newline at end of file diff --git a/web/src/components/infini/resize_bar.js b/web/src/components/infini/resize_bar.js new file mode 100644 index 00000000..d3bbde52 --- /dev/null +++ b/web/src/components/infini/resize_bar.js @@ -0,0 +1,9 @@ +import './resize_bar.scss'; + +export const ResizeBar = () => { + return
+
+
+
+
; +}; \ No newline at end of file diff --git a/web/src/components/infini/resize_bar.scss b/web/src/components/infini/resize_bar.scss new file mode 100644 index 00000000..ffa4abd4 --- /dev/null +++ b/web/src/components/infini/resize_bar.scss @@ -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; + } +} \ No newline at end of file diff --git a/web/src/components/kibana/console/components/CommonCommandModal.tsx b/web/src/components/kibana/console/components/CommonCommandModal.tsx index a44b5e75..0b3b54fe 100644 --- a/web/src/components/kibana/console/components/CommonCommandModal.tsx +++ b/web/src/components/kibana/console/components/CommonCommandModal.tsx @@ -66,7 +66,7 @@ const CommonCommandModal = Form.create()((props: ICommonCommandModalProps) => { }; return ( - +
{form.getFieldDecorator('title', { diff --git a/web/src/components/kibana/console/components/Console.tsx b/web/src/components/kibana/console/components/Console.tsx index 46c24bee..ef08fb6e 100644 --- a/web/src/components/kibana/console/components/Console.tsx +++ b/web/src/components/kibana/console/components/Console.tsx @@ -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(null); const consoleRef = useRef(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 ( -
-
+
+
@@ -82,12 +94,8 @@ const ConsoleWrapper = ({
-
- - +
+
- - +
); }; const Console = (params:props) => { + const registryRef = useRef(new PanelRegistry()); // const [consoleInputKey] = useMemo(()=>{ // return [selectedCluster.id + '-console-input']; diff --git a/web/src/components/kibana/console/components/ConsoleInput.tsx b/web/src/components/kibana/console/components/ConsoleInput.tsx index d5f6f11b..d0ea57c1 100644 --- a/web/src/components/kibana/console/components/ConsoleInput.tsx +++ b/web/src/components/kibana/console/components/ConsoleInput.tsx @@ -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, diff --git a/web/src/components/kibana/console/components/request_status_bar/request_status_bar.scss b/web/src/components/kibana/console/components/request_status_bar/request_status_bar.scss new file mode 100644 index 00000000..28841945 --- /dev/null +++ b/web/src/components/kibana/console/components/request_status_bar/request_status_bar.scss @@ -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; + } + } + } +} \ No newline at end of file diff --git a/web/src/components/kibana/console/components/request_status_bar/request_status_bar.tsx b/web/src/components/kibana/console/components/request_status_bar/request_status_bar.tsx index 0d98f85d..00a0baa5 100644 --- a/web/src/components/kibana/console/components/request_status_bar/request_status_bar.tsx +++ b/web/src/components/kibana/console/components/request_status_bar/request_status_bar.tsx @@ -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 = ({ +// export const RequestStatusBar: FunctionComponent = ({ +// requestInProgress, +// requestResult, +// selectedCluster, +// }) => { +// let content: React.ReactNode = null; +// const clusterContent = ( +// +// {selectedCluster.host} - {selectedCluster.version} +// +// ); + +// if (requestInProgress) { +// content = ( +// +// +// Request in progress +// +// +// ); +// } else if (requestResult) { +// const { endpoint, method, statusCode, statusText, timeElapsedMs } = requestResult; + +// content = ( +// <> +// +// {`${method} ${ +// endpoint.startsWith('/') ? endpoint : '/' + endpoint +// }`} +// } +// > +// +// {/* Use   to ensure that no matter the width we don't allow line breaks */} +// {statusCode} - {statusText} +// +// +// +// +// +// Time Elapsed +// +// } +// > +// +// +// {timeElapsedMs} {'ms'} +// +// +// +// +// +// ); +// } + +// return ( +// +// {clusterContent} +// {content} +// +// ); +// }; + +export const RequestStatusBar = ({ requestInProgress, requestResult, selectedCluster, -}) => { +}:Props) => { let content: React.ReactNode = null; - const clusterContent = ( - - {selectedCluster.host} - {selectedCluster.version} - -); + const clusterContent = (
+
+ 健康状态: + + + +
+
+ 集群地址: + {selectedCluster.host} +
+
+ 版本: + {selectedCluster.version} +
+
); +const [headerInfoVisible, setHeaderInfoVisible] = React.useState(false) if (requestInProgress) { content = ( @@ -85,7 +176,8 @@ export const RequestStatusBar: FunctionComponent = ({ content = ( <> - +
+
= ({ {statusCode} - {statusText} - - +
+
= ({ - +
+
+ + + +
+
); } return ( - - {clusterContent} - {content} - +
+
{clusterContent}
+
{content}
+ {setHeaderInfoVisible(false)}} + > + + +
+ + {requestResult?.requestHeader} + + +
+
+ + + {requestResult?.responseHeader} + + +
+
+
+ ); }; diff --git a/web/src/components/kibana/console/contexts/editor_context/editor_registry.ts b/web/src/components/kibana/console/contexts/editor_context/editor_registry.ts index b063cdeb..65eb4b8c 100644 --- a/web/src/components/kibana/console/contexts/editor_context/editor_registry.ts +++ b/web/src/components/kibana/console/contexts/editor_context/editor_registry.ts @@ -37,7 +37,6 @@ export class EditorRegistry { setInputEditor(inputEditor: SenseEditor) { this.inputEditor = inputEditor; - inputEditor.setAutocompleter(); } getInputEditor() { diff --git a/web/src/components/kibana/console/entities/core_editor.ts b/web/src/components/kibana/console/entities/core_editor.ts index 09c6cacb..b1e092ff 100644 --- a/web/src/components/kibana/console/entities/core_editor.ts +++ b/web/src/components/kibana/console/entities/core_editor.ts @@ -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. */ diff --git a/web/src/components/kibana/console/entities/sense_editor.ts b/web/src/components/kibana/console/entities/sense_editor.ts index 8a01e0ce..a734920c 100644 --- a/web/src/components/kibana/console/entities/sense_editor.ts +++ b/web/src/components/kibana/console/entities/sense_editor.ts @@ -12,16 +12,16 @@ export class SenseEditor { currentReqRange: (Range & { markerRef: unknown }) | null; parser: RowParser; - private readonly autocomplete: ReturnType; + // private readonly autocomplete: ReturnType; 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; diff --git a/web/src/components/kibana/console/hooks/use_send_current_request_to_es/index.ts b/web/src/components/kibana/console/hooks/use_send_current_request_to_es/index.ts index 0f3b04de..e160894d 100644 --- a/web/src/components/kibana/console/hooks/use_send_current_request_to_es/index.ts +++ b/web/src/components/kibana/console/hooks/use_send_current_request_to_es/index.ts @@ -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]); }; diff --git a/web/src/components/kibana/console/hooks/use_send_current_request_to_es/send_request_to_es.ts b/web/src/components/kibana/console/hooks/use_send_current_request_to_es/send_request_to_es.ts index 9fa2cf30..fb20e22c 100644 --- a/web/src/components/kibana/console/hooks/use_send_current_request_to_es/send_request_to_es.ts +++ b/web/src/components/kibana/console/hooks/use_send_current_request_to_es/send_request_to_es.ts @@ -74,7 +74,6 @@ export function sendRequestToES(args: EsRequestArgs): Promise if (reqId !== CURRENT_REQ_ID) { return; } - const xhr = dataOrjqXHR.promise ? dataOrjqXHR : jqXhrORerrorThrown; const isSuccess = @@ -83,7 +82,10 @@ export function sendRequestToES(args: EsRequestArgs): Promise ((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 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 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'; diff --git a/web/src/components/kibana/console/hooks/use_set_input_editor.ts b/web/src/components/kibana/console/hooks/use_set_input_editor.ts index d1c6d150..651358df 100644 --- a/web/src/components/kibana/console/hooks/use_set_input_editor.ts +++ b/web/src/components/kibana/console/hooks/use_set_input_editor.ts @@ -41,7 +41,7 @@ export const useSetInputEditor = () => { return useCallback( (editor: SenseEditor) => { dispatch({ type: 'setInputEditor', payload: editor }); - registry.setInputEditor(editor); + // registry.setInputEditor(editor); }, [dispatch] ); diff --git a/web/src/components/kibana/console/modules/autocomplete/autocomplete.ts b/web/src/components/kibana/console/modules/autocomplete/autocomplete.ts index 6d7d4595..679010b3 100644 --- a/web/src/components/kibana/console/modules/autocomplete/autocomplete.ts +++ b/web/src/components/kibana/console/modules/autocomplete/autocomplete.ts @@ -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; diff --git a/web/src/components/kibana/console/modules/es/index.ts b/web/src/components/kibana/console/modules/es/index.ts index 43d00643..8bbec3fc 100644 --- a/web/src/components/kibana/console/modules/es/index.ts +++ b/web/src/components/kibana/console/modules/es/index.ts @@ -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:{ diff --git a/web/src/components/kibana/console/modules/legacy_core_editor/legacy_core_editor.ts b/web/src/components/kibana/console/modules/legacy_core_editor/legacy_core_editor.ts index 16a7967f..148b8f8c 100644 --- a/web/src/components/kibana/console/modules/legacy_core_editor/legacy_core_editor.ts +++ b/web/src/components/kibana/console/modules/legacy_core_editor/legacy_core_editor.ts @@ -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; 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, diff --git a/web/src/components/kibana/console/modules/mappings/mappings.js b/web/src/components/kibana/console/modules/mappings/mappings.js index c4675e3a..f446b925 100644 --- a/web/src/components/kibana/console/modules/mappings/mappings.js +++ b/web/src/components/kibana/console/modules/mappings/mappings.js @@ -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 diff --git a/web/src/components/kibana/console/stores/editor.ts b/web/src/components/kibana/console/stores/editor.ts index e73ad115..06553b69 100644 --- a/web/src/components/kibana/console/stores/editor.ts +++ b/web/src/components/kibana/console/stores/editor.ts @@ -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( { ready: false, currentTextObject: null, + sensorEditor: null, }, identity ); @@ -58,7 +60,8 @@ export const reducer: Reducer = (state, action) => if (action.type === 'setInputEditor') { if (action.payload) { draft.ready = true; - } + draft.sensorEditor = action.payload; + } return; } diff --git a/web/src/lib/hooks/storage.js b/web/src/lib/hooks/storage.js index ac9805dd..2eb60de5 100644 --- a/web/src/lib/hooks/storage.js +++ b/web/src/lib/hooks/storage.js @@ -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 diff --git a/web/src/lib/hooks/use_event_listener.js b/web/src/lib/hooks/use_event_listener.js new file mode 100644 index 00000000..772ea587 --- /dev/null +++ b/web/src/lib/hooks/use_event_listener.js @@ -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]) +} \ No newline at end of file diff --git a/web/src/lib/hooks/use_timeout.js b/web/src/lib/hooks/use_timeout.js new file mode 100644 index 00000000..3b55c9e4 --- /dev/null +++ b/web/src/lib/hooks/use_timeout.js @@ -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 } +} \ No newline at end of file diff --git a/web/src/models/global.js b/web/src/models/global.js index bf19b45d..3f869dd1 100644 --- a/web/src/models/global.js +++ b/web/src/models/global.js @@ -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")){ diff --git a/web/src/pages/DevTool/Console.tsx b/web/src/pages/DevTool/Console.tsx index f687f58a..be144617 100644 --- a/web/src/pages/DevTool/Console.tsx +++ b/web/src/pages/DevTool/Console.tsx @@ -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)=>{ })} ); + + 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 =( - - ); + + {isFullscreen? + : + + } + {minimize? :null} +
+ ); 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 ( -
+ }} + onResize={onResize} + enable={{ + top: resizeable, + right: false, + bottom: false, + left: false, + topRight: false, + bottomRight: false, + bottomLeft: false, + topLeft: false, + }}> +
{ tabBarExtraContent={tabBarExtra} > {panes.map(pane => ( - - + {saveTitle(pane.key, title)}}/>} key={pane.key} closable={pane.closable}> + {/* {pane.content} */} ))}
+
); } @@ -188,4 +318,6 @@ export default connect(({ })=>({ selectedCluster: global.selectedCluster, clusterList: global.clusterList, + clusterStatus: global.clusterStatus, + height: window.innerHeight - 75 + 'px', }))(ConsoleUI); \ No newline at end of file diff --git a/web/src/pages/DevTool/console_tab_title.scss b/web/src/pages/DevTool/console_tab_title.scss new file mode 100644 index 00000000..9d20d107 --- /dev/null +++ b/web/src/pages/DevTool/console_tab_title.scss @@ -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; + } +} \ No newline at end of file diff --git a/web/src/pages/DevTool/console_tab_title.tsx b/web/src/pages/DevTool/console_tab_title.tsx new file mode 100644 index 00000000..1f994503 --- /dev/null +++ b/web/src/pages/DevTool/console_tab_title.tsx @@ -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 (
{ + setEditable(true) + }}> + {editable ? { + setEditable(false) + }} + onChange={onValueChange}/>:value} +
) +} +