modify discover and console

This commit is contained in:
liugq 2021-11-11 15:45:35 +08:00
parent 8685936eb1
commit 354bcd87bb
42 changed files with 2300 additions and 1198 deletions

View File

@ -5,7 +5,7 @@ import (
log "github.com/cihub/seelog"
httprouter "infini.sh/framework/core/api/router"
"infini.sh/framework/core/elastic"
"infini.sh/framework/core/metrics"
"infini.sh/framework/core/event"
"infini.sh/framework/core/orm"
"infini.sh/framework/core/util"
"net/http"
@ -77,7 +77,7 @@ func (handler APIHandler) getLatestClusterMonitorData(clusterID interface{}) (ut
]
}`
queryDSL := fmt.Sprintf(queryDSLTpl, clusterID)
searchRes, err := client.SearchWithRawQueryDSL(orm.GetIndexName(metrics.MetricEvent{}), []byte(queryDSL))
searchRes, err := client.SearchWithRawQueryDSL(orm.GetIndexName(event.Event{}), []byte(queryDSL))
if err != nil {
return nil, err
}

View File

@ -87,7 +87,7 @@ func main() {
}, func() {
orm.RegisterSchemaWithIndexName(model.Dict{}, "dict")
orm.RegisterSchemaWithIndexName(model.Reindex{}, "reindex")
orm.RegisterSchemaWithIndexName(elastic.IndexPattern{}, "view")
orm.RegisterSchemaWithIndexName(elastic.View{}, "view")
orm.RegisterSchemaWithIndexName(alerting.Config{}, "alerting-config")
orm.RegisterSchemaWithIndexName(alerting.Alert{}, "alerting-alerts")
orm.RegisterSchemaWithIndexName(alerting.AlertingHistory{}, "alerting-alert-history")

View File

@ -33,19 +33,10 @@ export default [
// { path: '/', redirect: '/' },
// ],
// },
{
path: '/cluster/overview_new',
name: 'overview',
component: './Cluster/NewOverview',
// hideInMenu: true,
routes:[
{ path: '/', redirect: '/' },
],
},
{
path: '/cluster/overview',
name: 'overview',
component: './Cluster/Overview',
component: './Cluster/NewOverview',
// hideInMenu: true,
routes:[
{ path: '/', redirect: '/' },

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -0,0 +1 @@
<svg width="24px" height="24px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M21,2H3A1,1,0,0,0,2,3V21a1,1,0,0,0,1,1H21a1,1,0,0,0,1-1V3A1,1,0,0,0,21,2ZM20,20H4V10H20ZM20,8H4V4H20Z"/></svg>

After

Width:  |  Height:  |  Size: 206 B

View File

@ -0,0 +1,4 @@
<svg width="512px" height="512px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<path fill="var(--ci-primary-color, currentColor)" d="M352,153H40.247a24.028,24.028,0,0,0-24,24V458a24.028,24.028,0,0,0,24,24H352a24.028,24.028,0,0,0,24-24V177A24.028,24.028,0,0,0,352,153Zm-8,32v45.22H48.247V185ZM48.247,450V262.22H344V450Z" class="ci-primary"/>
<path fill="var(--ci-primary-color, currentColor)" d="M472,32H152a24.028,24.028,0,0,0-24,24v65h32V64H464V339.143H408v32h64a24.028,24.028,0,0,0,24-24V56A24.028,24.028,0,0,0,472,32Z" class="ci-primary"/>
</svg>

After

Width:  |  Height:  |  Size: 567 B

View File

@ -2,6 +2,7 @@
display: flex;
height: 10px;
background: #eee;
margin-top: -5px;
// border-top: 1px solid #bbb;
align-items: center;
justify-content: center;

View File

@ -1,24 +1,55 @@
.ant-tabs-top-bar{
.ant-tabs-top-bar {
display: flex;
}
.ant-tabs-extra-left{
.ant-tabs-extra-left {
order: 1;
flex: 0 0 auto;
}
.ant-tabs-nav-container{
.ant-tabs-nav-container {
order: 2;
flex: 0 0 auto;
}
.flex-tabbar .ant-tabs-top-bar .ant-tabs-nav-container{
max-width: calc(100% - 140px);
.flex-tabbar {
.ant-tabs-top-bar .ant-tabs-nav-container {
max-width: calc(100% - 140px);
}
.tabbar-icon {
padding: 0 10px;
cursor: pointer;
}
.tabbar-icon:hover {
background-color: #efefef;
}
}
.ant-tabs-extra-append{
.ant-tabs-extra-append {
order: 3;
margin-left: 10px;
}
.ant-tabs-extra-right{
.ant-tabs-extra-right {
order: 4;
margin-left: auto;
flex: 0 0 auto;
}
}
#console {
.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab-active {
font-weight: 400;
background-color: #939ea0;
color: #fff;
border-color: #939ea0;
}
.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab {
border-radius: 0;
height: 32px;
}
.ant-tabs.ant-tabs-card .ant-tabs-extra-content {
line-height: 30px;
display: flex;
}
.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-nav-container {
height: 30px;
}
.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab .ant-tabs-close-x {
margin-bottom: 18px;
}
}

View File

@ -55,9 +55,9 @@ const ConsoleWrapper = ({
<div style={{height: calcHeight}}>
<div className="Console" style={{height:'100%'}}>
<PanelsContainer resizerClassName="resizer">
<Panel style={{ height: '100%', position: 'relative', minWidth: PANEL_MIN_WIDTH, paddingBottom:30 }} initialWidth={INITIAL_PANEL_WIDTH}>
<ConsoleInput clusterID={selectedCluster.id} saveEditorContent={saveEditorContent} initialText={initialText} paneKey={paneKey} />
<div style={{background:'#fff', position:'absolute', left:0, bottom:0, width: '100%', height:30, zIndex:1001, borderTop: '1px solid #eee'}}>
<Panel style={{ height: '100%', position: 'relative', minWidth: PANEL_MIN_WIDTH, paddingBottom:26 }} initialWidth={INITIAL_PANEL_WIDTH}>
<ConsoleInput height={height-26+'px'} clusterID={selectedCluster.id} saveEditorContent={saveEditorContent} initialText={initialText} paneKey={paneKey} />
<div style={{background:'#fff', position:'absolute', left:0, bottom:0, width: '100%', height:26, zIndex:997, borderTop: '1px solid #eee'}}>
<RequestStatusBar
requestInProgress={requestInProgress}
selectedCluster={selectedCluster}
@ -78,10 +78,10 @@ const ConsoleWrapper = ({
/>
</div>
</Panel>
<Panel style={{ height: '100%', position: 'relative', minWidth: PANEL_MIN_WIDTH, paddingBottom:30 }} initialWidth={INITIAL_PANEL_WIDTH}>
<Panel style={{ height: '100%', position: 'relative', minWidth: PANEL_MIN_WIDTH, paddingBottom:26 }} initialWidth={INITIAL_PANEL_WIDTH}>
<Tabs tabPosition='right' style={{height:'100%', width:'100%'}} size="small">
<Tabs.TabPane tab="Result" key="result">
<div style={{height:height-30}}>
<div style={{height:height-26}}>
<ConsoleOutput clusterID={selectedCluster.id} />
</div>
@ -101,7 +101,7 @@ const ConsoleWrapper = ({
</Tabs>
</Tabs.TabPane>
</Tabs>
<div style={{background:'#fff', position:'absolute', right:0, bottom:0, width: '100%', height:30, zIndex:1001, borderTop: '1px solid #eee'}}>
<div style={{background:'#fff', position:'absolute', right:0, bottom:0, width: '100%', height:26, zIndex:997, borderTop: '1px solid #eee'}}>
<RequestStatusBar
requestInProgress={requestInProgress}
selectedCluster={selectedCluster}

View File

@ -1,7 +1,7 @@
// @ts-ignore
import React, { useRef, useEffect, CSSProperties, useMemo } from 'react';
import ace from 'brace';
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiToolTip } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiToolTip, PropertySortType } from '@elastic/eui';
import { SenseEditor } from '../entities/sense_editor';
import { LegacyCoreEditor } from '../modules/legacy_core_editor/legacy_core_editor';
import ConsoleMenu from './ConsoleMenu';
@ -68,6 +68,7 @@ interface ConsoleInputProps {
initialText: string | undefined,
saveEditorContent: (content: string)=>void,
paneKey: string,
height?: string,
}
const DEFAULT_INPUT_VALUE = `GET _search
@ -78,7 +79,7 @@ const DEFAULT_INPUT_VALUE = `GET _search
}`;
const ConsoleInputUI = ({clusterID, initialText, saveEditorContent, paneKey}:ConsoleInputProps) => {
const ConsoleInputUI = ({clusterID, initialText, saveEditorContent, paneKey, height='100%'}:ConsoleInputProps) => {
const editorRef = useRef<HTMLDivElement | null>(null);
const editorActionsRef = useRef<HTMLDivElement | null>(null);
const editorInstanceRef = useRef<SenseEditor | null>(null);
@ -167,7 +168,7 @@ const ConsoleInputUI = ({clusterID, initialText, saveEditorContent, paneKey}:Con
return (
<div style={abs} data-test-subj="console-application" className="conApp">
<div style={{...abs, height: height}} data-test-subj="console-application" className="conApp">
<div className="conApp__editor">
<ul className="conApp__autoComplete" id="autocomplete" />
<EuiFlexGroup

View File

@ -161,17 +161,6 @@ export default class ConsoleMenu extends Component<Props, State> {
);
const items = [
<EuiContextMenuItem
key="Copy as cURL"
id="ConCopyAsCurl"
disabled={!window.navigator?.clipboard}
onClick={() => {
this.closePopover();
this.copyAsCurl();
}}
>
curl命令
</EuiContextMenuItem>,
<EuiContextMenuItem
key="Auto indent"
onClick={this.autoIndent}
@ -185,6 +174,19 @@ export default class ConsoleMenu extends Component<Props, State> {
</EuiContextMenuItem>,
];
if(window.navigator?.clipboard){
items.unshift(<EuiContextMenuItem
key="Copy as cURL"
id="ConCopyAsCurl"
disabled={!window.navigator?.clipboard}
onClick={() => {
this.closePopover();
this.copyAsCurl();
}}
>
curl命令
</EuiContextMenuItem>)
}
return (
<span onMouseEnter={this.mouseEnter}>

View File

@ -10,6 +10,7 @@
.info-item{
margin: 0 12px;
position: relative;
flex: 0 0 auto;
&.health{
padding-right: 14px;
}

View File

@ -17,12 +17,19 @@
* under the License.
*/
import React, { FunctionComponent } from 'react';
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';
import { FormattedMessage, formatMessage } from 'umi/locale';
import React, { FunctionComponent } from "react";
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";
import { FormattedMessage, formatMessage } from "umi/locale";
export interface Props {
requestInProgress: boolean;
@ -48,22 +55,22 @@ export interface Props {
const mapStatusCodeToBadgeColor = (statusCode: number) => {
if (statusCode <= 199) {
return 'default';
return "default";
}
if (statusCode <= 299) {
return 'secondary';
return "secondary";
}
if (statusCode <= 399) {
return 'primary';
return "primary";
}
if (statusCode <= 499) {
return 'warning';
return "warning";
}
return 'danger';
return "danger";
};
export const RequestStatusBar = ({
@ -71,74 +78,87 @@ export const RequestStatusBar = ({
requestResult,
selectedCluster,
left,
}:Props) => {
}: Props) => {
let content: React.ReactNode = null;
const clusterContent = (<div className="base-info">
const clusterContent = (
<div className="base-info">
<div className="info-item health">
<span> <FormattedMessage id="console.cluster.status"/></span>
<i style={{position:'absolute', top: 1, right:0}}>
<HealthStatusCircle status={selectedCluster.status}/>
<span>
{" "}
<FormattedMessage id="console.cluster.status" />
</span>
<i style={{ position: "absolute", top: 1, right: 0 }}>
<HealthStatusCircle status={selectedCluster.status} />
</i>
</div>
<div className="info-item">
<span><FormattedMessage id="console.cluster.endpoint"/></span>
<EuiBadge color="default">{selectedCluster.host}</EuiBadge>
<span>
<FormattedMessage id="console.cluster.endpoint" />
</span>
<EuiBadge color="default">{selectedCluster.host}</EuiBadge>
</div>
<div className="info-item">
<span><FormattedMessage id="console.cluster.version"/></span>
<span>
<FormattedMessage id="console.cluster.version" />
</span>
<EuiBadge color="default">{selectedCluster.version}</EuiBadge>
</div>
</div>);
</div>
</div>
);
if (requestInProgress) {
content = (
<EuiFlexItem grow={false}>
<EuiBadge color="hollow">
Request in progress
</EuiBadge>
<EuiBadge color="hollow">Request in progress</EuiBadge>
</EuiFlexItem>
);
} else if (requestResult) {
const { endpoint, method, statusCode, statusText, timeElapsedMs } = requestResult;
const {
endpoint,
method,
statusCode,
statusText,
timeElapsedMs,
} = requestResult;
content = (
<>
<div className="status_info">
<div className="info-item">
<span><FormattedMessage id="console.response.status"/></span>
<EuiToolTip
position="top"
content={
<EuiText size="s">{`${method} ${
endpoint.startsWith('/') ? endpoint : '/' + endpoint
}`}</EuiText>
}
>
<EuiText size="s">
<EuiBadge color={mapStatusCodeToBadgeColor(statusCode)}>
{/* Use &nbsp; to ensure that no matter the width we don't allow line breaks */}
{statusCode}&nbsp;-&nbsp;{statusText}
</EuiBadge>
</EuiText>
</EuiToolTip>
</div>
<div className="info-item">
<span><FormattedMessage id="console.response.time_elapsed"/></span>
<EuiToolTip
position="top"
content={
<div className="status_info">
<div className="info-item">
<span>
<FormattedMessage id="console.response.status" />
</span>
<EuiToolTip
position="top"
content={
<EuiText size="s">{`${method} ${
endpoint.startsWith("/") ? endpoint : "/" + endpoint
}`}</EuiText>
}
>
<EuiText size="s">
Time Elapsed
<EuiBadge color={mapStatusCodeToBadgeColor(statusCode)}>
{/* Use &nbsp; to ensure that no matter the width we don't allow line breaks */}
{statusCode}&nbsp;-&nbsp;{statusText}
</EuiBadge>
</EuiText>
}
>
<EuiText size="s">
<EuiBadge color="default">
{timeElapsedMs}&nbsp;{'ms'}
</EuiBadge>
</EuiText>
</EuiToolTip>
</div>
</EuiToolTip>
</div>
<div className="info-item">
<span>
<FormattedMessage id="console.response.time_elapsed" />
</span>
<EuiToolTip
position="top"
content={<EuiText size="s">Time Elapsed</EuiText>}
>
<EuiText size="s">
<EuiBadge color="default">
{timeElapsedMs}&nbsp;{"ms"}
</EuiBadge>
</EuiText>
</EuiToolTip>
</div>
</div>
</>
);
@ -146,10 +166,11 @@ export const RequestStatusBar = ({
return (
<div className="request-status-bar">
{left? <div className="bar-item">{clusterContent}</div>:
[<div className="bar-item">{content}</div>,]
}
{left ? (
<div className="bar-item">{clusterContent}</div>
) : (
<div className="bar-item">{content}</div>
)}
</div>
);
};

View File

@ -62,7 +62,7 @@ export const useSendCurrentRequestToES = () => {
}
const {url, method, data} = requests[0];
if(method === 'LOAD'){
const rawUrl = data[0].slice(4).trim();
const rawUrl = data[0]? data[0].slice(4).trim(): url;
const cmd = getCommand(rawUrl);
// const curPostion = editor.currentReqRange //(editor.getCoreEditor().getCurrentPosition());
const lineNumber = editor.getCoreEditor().getCurrentPosition().lineNumber;

View File

@ -17,14 +17,14 @@
* under the License.
*/
import { SavedObjectsClientCommon } from '../..';
import { SavedObjectsClientCommon } from "../..";
import { createIndexPatternCache } from '.';
import { IndexPattern } from './index_pattern';
import { createIndexPatternCache } from ".";
import { IndexPattern } from "./index_pattern";
import {
createEnsureDefaultIndexPattern,
EnsureDefaultIndexPattern,
} from './ensure_default_index_pattern';
} from "./ensure_default_index_pattern";
import {
OnNotification,
OnError,
@ -37,17 +37,17 @@ import {
FieldSpec,
FieldFormatMap,
IndexPatternFieldMap,
} from '../types';
import { FieldFormatsStartCommon } from '../../field_formats';
import { UI_SETTINGS, SavedObject } from '../../../common';
import { SavedObjectNotFound } from '../../../../kibana_utils/common';
import { IndexPatternMissingIndices } from '../lib';
import { findByTitle } from '../utils';
import { DuplicateIndexPatternError } from '../errors';
} from "../types";
import { FieldFormatsStartCommon } from "../../field_formats";
import { UI_SETTINGS, SavedObject } from "../../../common";
import { SavedObjectNotFound } from "../../../../kibana_utils/common";
import { IndexPatternMissingIndices } from "../lib";
import { findByTitle } from "../utils";
import { DuplicateIndexPatternError } from "../errors";
const indexPatternCache = createIndexPatternCache();
const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3;
const savedObjectType = 'view';
const savedObjectType = "view";
export interface IndexPatternSavedObjectAttrs {
title: string;
@ -67,7 +67,9 @@ interface IndexPatternsServiceDeps {
export class IndexPatternsService {
private config: UiSettingsCommon;
private savedObjectsClient: SavedObjectsClientCommon;
private savedObjectsCache?: Array<SavedObject<IndexPatternSavedObjectAttrs>> | null;
private savedObjectsCache?: Array<
SavedObject<IndexPatternSavedObjectAttrs>
> | null;
private apiClient: IIndexPatternsApiClient;
private fieldFormats: FieldFormatsStartCommon;
private onNotification: OnNotification;
@ -102,9 +104,11 @@ export class IndexPatternsService {
* Refresh cache of index pattern ids and titles
*/
private async refreshSavedObjectsCache() {
this.savedObjectsCache = await this.savedObjectsClient.find<IndexPatternSavedObjectAttrs>({
type: 'index-pattern',
fields: ['title'],
this.savedObjectsCache = await this.savedObjectsClient.find<
IndexPatternSavedObjectAttrs
>({
type: "index-pattern",
fields: ["title"],
perPage: 10000,
});
}
@ -180,7 +184,7 @@ export class IndexPatternsService {
* Get default index pattern
*/
getDefault = async () => {
const defaultIndexPatternId = await this.config.get('defaultIndex');
const defaultIndexPatternId = await this.config.get("defaultIndex");
if (defaultIndexPatternId) {
return await this.get(defaultIndexPatternId);
}
@ -194,8 +198,8 @@ export class IndexPatternsService {
* @param force
*/
setDefault = async (id: string, force = false) => {
if (force || !this.config.get('defaultIndex')) {
await this.config.set('defaultIndex', id);
if (force || !this.config.get("defaultIndex")) {
await this.config.set("defaultIndex", id);
}
};
@ -206,10 +210,10 @@ export class IndexPatternsService {
return Object.values(specs).every((spec) => {
// See https://github.com/elastic/kibana/pull/8421
const hasFieldCaps = 'aggregatable' in spec && 'searchable' in spec;
const hasFieldCaps = "aggregatable" in spec && "searchable" in spec;
// See https://github.com/elastic/kibana/pull/11969
const hasDocValuesFlag = 'readFromDocValues' in spec;
const hasDocValuesFlag = "readFromDocValues" in spec;
return !hasFieldCaps || !hasDocValuesFlag;
});
@ -251,15 +255,22 @@ export class IndexPatternsService {
refreshFields = async (indexPattern: IndexPattern) => {
try {
const fields = await this.getFieldsForIndexPattern(indexPattern);
const scripted = indexPattern.getScriptedFields().map((field) => field.spec);
const scripted = indexPattern
.getScriptedFields()
.map((field) => field.spec);
indexPattern.fields.replaceAll([...fields, ...scripted]);
} catch (err) {
if (err instanceof IndexPatternMissingIndices) {
this.onNotification({ title: (err as any).message, color: 'danger', iconType: 'alert' });
this.onNotification({
title: (err as any).message,
color: "danger",
iconType: "alert",
});
}
this.onError(err, {
title: 'Error fetching fields for index pattern {indexPattern.title} (ID: {indexPattern.id})',
title:
"Error fetching fields for index pattern {indexPattern.title} (ID: {indexPattern.id})",
});
}
};
@ -277,18 +288,24 @@ export class IndexPatternsService {
title: string,
options: GetFieldsOptions
) => {
const scriptdFields = Object.values(fields).filter((field) => field.scripted);
const scriptdFields = Object.values(fields).filter(
(field) => field.scripted
);
try {
const newFields = await this.getFieldsForWildcard(options);
return this.fieldArrayToMap([...newFields, ...scriptdFields]);
} catch (err) {
if (err instanceof IndexPatternMissingIndices) {
this.onNotification({ title: (err as any).message, color: 'danger', iconType: 'alert' });
this.onNotification({
title: (err as any).message,
color: "danger",
iconType: "alert",
});
return {};
}
this.onError(err, {
title: `Error fetching fields for index pattern ${title} (ID: ${id})`
title: `Error fetching fields for index pattern ${title} (ID: ${id})`,
});
}
return fields;
@ -299,7 +316,10 @@ export class IndexPatternsService {
* @param fieldSpecs
* @param fieldFormatMap
*/
private addFormatsToFields = (fieldSpecs: FieldSpec[], fieldFormatMap: FieldFormatMap) => {
private addFormatsToFields = (
fieldSpecs: FieldSpec[],
fieldFormatMap: FieldFormatMap
) => {
Object.entries(fieldFormatMap).forEach(([fieldName, value]) => {
const field = fieldSpecs.find((fld: FieldSpec) => fld.name === fieldName);
if (field) {
@ -323,7 +343,9 @@ export class IndexPatternsService {
* @param savedObject
*/
savedObjectToSpec = (savedObject: SavedObject<IndexPatternAttributes>): IndexPatternSpec => {
savedObjectToSpec = (
savedObject: SavedObject<IndexPatternAttributes>
): IndexPatternSpec => {
const {
id,
version,
@ -336,13 +358,17 @@ export class IndexPatternsService {
sourceFilters,
fieldFormatMap,
typeMeta,
type,
},
type,
} = savedObject;
const parsedSourceFilters = sourceFilters ? JSON.parse(sourceFilters) : undefined;
const parsedSourceFilters = sourceFilters
? JSON.parse(sourceFilters)
: undefined;
const parsedTypeMeta = typeMeta ? JSON.parse(typeMeta) : undefined;
const parsedFieldFormatMap = fieldFormatMap ? JSON.parse(fieldFormatMap) : {};
const parsedFieldFormatMap = fieldFormatMap
? JSON.parse(fieldFormatMap)
: {};
const parsedFields: FieldSpec[] = fields ? JSON.parse(fields) : [];
this.addFormatsToFields(parsedFields, parsedFieldFormatMap);
@ -365,16 +391,20 @@ export class IndexPatternsService {
* @param id
*/
get = async (id: string): Promise<IndexPattern> => {
const cache = indexPatternCache.get(id);
get = async (
id: string,
typ: string,
clusterID: string
): Promise<IndexPattern> => {
const cacheID = typ == "index" ? clusterID + id : id;
const cache = indexPatternCache.get(cacheID);
if (cache) {
return cache;
}
const savedObject = await this.savedObjectsClient.get<IndexPatternAttributes>(
savedObjectType,
id
);
const savedObject = await this.savedObjectsClient.get<
IndexPatternAttributes
>(typ || savedObjectType, id);
// if (!savedObject.version) {
// throw new SavedObjectNotFound(savedObjectType, id, 'management/kibana/indexPatterns');
@ -382,28 +412,34 @@ export class IndexPatternsService {
const spec = this.savedObjectToSpec(savedObject);
const { title, type, typeMeta } = spec;
const parsedFieldFormats: FieldFormatMap = savedObject.attributes.fieldFormatMap
const parsedFieldFormats: FieldFormatMap = savedObject.attributes
.fieldFormatMap
? JSON.parse(savedObject.attributes.fieldFormatMap)
: {};
const isFieldRefreshRequired = this.isFieldRefreshRequired(spec.fields);
const isFieldRefreshRequired =
spec.type == "index" ? false : this.isFieldRefreshRequired(spec.fields);
let isSaveRequired = isFieldRefreshRequired;
try {
spec.fields = isFieldRefreshRequired
? await this.refreshFieldSpecMap(spec.fields || {}, id, spec.title as string, {
pattern: title,
metaFields: await this.config.get(UI_SETTINGS.META_FIELDS),
type,
params: typeMeta && typeMeta.params,
})
? await this.refreshFieldSpecMap(
spec.fields || {},
id,
spec.title as string,
{
pattern: title,
metaFields: await this.config.get(UI_SETTINGS.META_FIELDS),
type,
params: typeMeta && typeMeta.params,
}
)
: spec.fields;
} catch (err) {
isSaveRequired = false;
if (err instanceof IndexPatternMissingIndices) {
this.onNotification({
title: (err as any).message,
color: 'danger',
iconType: 'alert',
color: "danger",
iconType: "alert",
});
} else {
this.onError(err, {
@ -468,8 +504,13 @@ export class IndexPatternsService {
* @param spec
* @param skipFetchFields
*/
async create(spec: IndexPatternSpec, skipFetchFields = false): Promise<IndexPattern> {
const shortDotsEnable = await this.config.get(UI_SETTINGS.SHORT_DOTS_ENABLE);
async create(
spec: IndexPatternSpec,
skipFetchFields = false
): Promise<IndexPattern> {
const shortDotsEnable = await this.config.get(
UI_SETTINGS.SHORT_DOTS_ENABLE
);
const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS);
const indexPattern = new IndexPattern({
@ -488,11 +529,13 @@ export class IndexPatternsService {
}
find = async (search: string, size: number = 10): Promise<IndexPattern[]> => {
const savedObjects = await this.savedObjectsClient.find<IndexPatternSavedObjectAttrs>({
type: 'index-pattern',
fields: ['title'],
const savedObjects = await this.savedObjectsClient.find<
IndexPatternSavedObjectAttrs
>({
type: "index-pattern",
fields: ["title"],
search,
searchFields: ['title'],
searchFields: ["title"],
perPage: size,
});
const getIndexPatternPromises = savedObjects.map(async (savedObject) => {
@ -508,7 +551,11 @@ export class IndexPatternsService {
* @param skipFetchFields
*/
async createAndSave(spec: IndexPatternSpec, override = false, skipFetchFields = false) {
async createAndSave(
spec: IndexPatternSpec,
override = false,
skipFetchFields = false
) {
const indexPattern = await this.create(spec, skipFetchFields);
await this.createSavedObject(indexPattern, override);
await this.setDefault(indexPattern.id as string);
@ -527,14 +574,20 @@ export class IndexPatternsService {
if (override) {
await this.delete(dupe.id);
} else {
throw new DuplicateIndexPatternError(`Duplicate index pattern: ${indexPattern.title}`);
throw new DuplicateIndexPatternError(
`Duplicate index pattern: ${indexPattern.title}`
);
}
}
const body = indexPattern.getAsSavedObjectBody();
const response = await this.savedObjectsClient.create(savedObjectType, body, {
id: indexPattern.id,
});
const response = await this.savedObjectsClient.create(
savedObjectType,
body,
{
id: indexPattern.id,
}
);
indexPattern.id = response.id;
indexPatternCache.set(indexPattern.id, indexPattern);
return indexPattern;
@ -551,6 +604,7 @@ export class IndexPatternsService {
saveAttempts: number = 0,
ignoreErrors: boolean = false
): Promise<void | Error> {
debugger;
if (!indexPattern.id) return;
// get the list of attributes
@ -566,13 +620,18 @@ export class IndexPatternsService {
});
return this.savedObjectsClient
.update(savedObjectType, indexPattern.id, body, { version: indexPattern.version })
.update(savedObjectType, indexPattern.id, body, {
version: indexPattern.version,
})
.then((resp) => {
indexPattern.id = resp.id;
indexPattern.version = resp.version;
})
.catch(async (err) => {
if (err?.res?.status === 409 && saveAttempts++ < MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS) {
if (
err?.res?.status === 409 &&
saveAttempts++ < MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS
) {
const samePattern = await this.get(indexPattern.id as string);
// What keys changed from now and what the server returned
const updatedBody = samePattern.getAsSavedObjectBody();
@ -584,7 +643,10 @@ export class IndexPatternsService {
const serverChangedKeys: string[] = [];
Object.entries(updatedBody).forEach(([key, value]) => {
if (value !== (body as any)[key] && value !== (originalBody as any)[key]) {
if (
value !== (body as any)[key] &&
value !== (originalBody as any)[key]
) {
serverChangedKeys.push(key);
}
});
@ -603,10 +665,10 @@ export class IndexPatternsService {
if (ignoreErrors) {
return;
}
const title =
'Unable to write index pattern! Refresh the page to get the most up to date changes for this index pattern.';
const title =
"Unable to write index pattern! Refresh the page to get the most up to date changes for this index pattern.";
this.onNotification({ title, color: 'danger' });
this.onNotification({ title, color: "danger" });
throw err;
}
@ -620,7 +682,11 @@ export class IndexPatternsService {
indexPatternCache.clear(indexPattern.id!);
// Try the save again
return this.updateSavedObject(indexPattern, saveAttempts, ignoreErrors);
return this.updateSavedObject(
indexPattern,
saveAttempts,
ignoreErrors
);
}
throw err;
});

View File

@ -185,7 +185,7 @@ function FilterBarUI(props: Props) {
alignItems="flexStart"
responsive={false}
>
<EuiFlexItem className="globalFilterGroup__branch" grow={false}>
<EuiFlexItem className="globalFilterGroup__branch" grow={false} style={{padding:0, alignSelf:'center'}}>
<FilterOptions
onEnableAll={onEnableAll}
onDisableAll={onDisableAll}

View File

@ -1,10 +1,11 @@
import { EuiIcon } from "@elastic/eui";
import { TableHeader } from "./table_header/table_header";
import { SortOrder } from "./table_header/helpers";
import './_doc_table.scss';
import {TableRow} from './table_row/table_row';
import { useState, useEffect} from 'react';
import InfiniteScroll from 'react-infinite-scroll-component';
import "./_doc_table.scss";
import { TableRow } from "./table_row/table_row";
import React, { useState, useEffect } from "react";
import InfiniteScroll from "react-infinite-scroll-component";
import TableContext from "./table_context";
interface TableProps {
columns: string[];
@ -21,61 +22,83 @@ interface TableProps {
const pageCount = 50;
const Table: React.FC<TableProps> = ({ columns, hits, sortOrder, indexPattern, onFilter, onMoveColumn, onAddColumn,
onRemoveColumn, onChangeSortOrder, document }) => {
const [scrollState, setScrollState] = useState({limit: pageCount, hasMore: true});
useEffect(()=>{
const Table: React.FC<TableProps> = ({
columns,
hits,
sortOrder,
indexPattern,
onFilter,
onMoveColumn,
onAddColumn,
onRemoveColumn,
onChangeSortOrder,
document,
}) => {
const [scrollState, setScrollState] = useState({
limit: pageCount,
hasMore: true,
});
useEffect(() => {
setScrollState({
limit: pageCount,
limit: pageCount,
hasMore: hits.length > pageCount,
})
},[indexPattern, hits])
});
}, [indexPattern, hits]);
const tableRef = React.useRef(null);
return (
<InfiniteScroll
dataLength={scrollState.limit}
next={()=>{
next={() => {
const newLimit = scrollState.limit + pageCount;
setScrollState({limit: newLimit, hasMore: newLimit < hits.length});
setScrollState({ limit: newLimit, hasMore: newLimit < hits.length });
}}
hasMore={scrollState.hasMore}
loader={<h4 style={{textAlign: 'center', margin: '10px auto'}}>Loading...</h4>}
loader={
<h4 style={{ textAlign: "center", margin: "10px auto" }}>Loading...</h4>
}
endMessage={
<p style={{ textAlign: 'center' }}>
{/* <b>no more data</b> */}
</p>
}>
<div>
<p style={{ textAlign: "center" }}>{/* <b>no more data</b> */}</p>
}
>
<div ref={tableRef}>
{hits.length ? (
<div>
<table className="kbn-table table">
<thead>
<TableHeader columns={columns}
defaultSortOrder={''}
hideTimeColumn={false}
indexPattern={indexPattern}
isShortDots={false}
onChangeSortOrder={onChangeSortOrder}
onMoveColumn={onMoveColumn}
onRemoveColumn={onRemoveColumn}
sortOrder={sortOrder||[]}/>
</thead>
<tbody>
{hits.slice(0, scrollState.limit).map((row, idx)=>{
return <TableRow key={'discover-table-row'+row._id} onFilter={onFilter}
columns={columns}
hideTimeColumn={false}
indexPattern={indexPattern}
isShortDots={false}
onAddColumn={onAddColumn}
onRemoveColumn={onRemoveColumn}
row={row}
document={document}
<TableContext.Provider value={{ tableRef: tableRef.current }}>
<table className="kbn-table table">
<thead>
<TableHeader
columns={columns}
defaultSortOrder={"desc"}
hideTimeColumn={false}
indexPattern={indexPattern}
isShortDots={false}
onChangeSortOrder={onChangeSortOrder}
onMoveColumn={onMoveColumn}
onRemoveColumn={onRemoveColumn}
sortOrder={sortOrder || []}
/>
})}
</tbody>
</table>
</thead>
<tbody>
{hits.slice(0, scrollState.limit).map((row, idx) => {
return (
<TableRow
key={"discover-table-row" + row._id}
onFilter={onFilter}
columns={columns}
hideTimeColumn={false}
indexPattern={indexPattern}
isShortDots={false}
onAddColumn={onAddColumn}
onRemoveColumn={onRemoveColumn}
row={row}
document={document}
/>
);
})}
</tbody>
</table>
</TableContext.Provider>
</div>
) : null}
@ -89,8 +112,8 @@ const Table: React.FC<TableProps> = ({ columns, hits, sortOrder, indexPattern, o
</div>
) : null}
</div>
</InfiniteScroll>
</InfiniteScroll>
);
};
export default Table;
export default React.memo(Table);

View File

@ -0,0 +1,7 @@
import * as React from "react";
const TableContext = React.createContext({
tableRef: null,
});
export default TableContext;

View File

@ -8,7 +8,6 @@
// Overwrite the border on the bootstrap table
.kbnDocTableDetails__row {
> td {
// Offsets negative margins from an inner flex group
padding: 16px !important;
@ -22,4 +21,3 @@
border-top: none !important;
}
}

View File

@ -1,15 +1,29 @@
import {EuiIcon} from '@elastic/eui';
import { DocViewer } from '../../doc_viewer/doc_viewer';
import {Drawer, Button, Menu,Dropdown, Icon, Popconfirm, message,Descriptions, Popover, Input} from 'antd';
import { EuiIcon } from "@elastic/eui";
import { DocViewer } from "../../doc_viewer/doc_viewer";
import {
Drawer,
Button,
Menu,
Dropdown,
Icon,
Popconfirm,
message,
Descriptions,
Popover,
Input,
} from "antd";
import Editor from "@monaco-editor/react";
import {useState, useRef} from 'react';
import { useState, useRef } from "react";
function generateNewID(id: string) {
return id.slice(0, 14) + Math.random().toString(36).substr(2, 6)
return (
id.slice(0, 14) +
Math.random()
.toString(36)
.substr(2, 6)
);
}
interface Props {
columns: string[];
indexPattern: any;
@ -28,28 +42,28 @@ export function Detail({
onAddColumn,
onRemoveColumn,
document,
}:Props){
}: Props) {
const [editorVisible, setEditorVisble] = useState(false);
const editorRef = useRef(null);
function handleEditorDidMount(editor, monaco) {
editorRef.current = editor;
editorRef.current = editor;
}
const editDocumentClick = ()=>{
setEditorVisble(true)
}
const editCancelClick = ()=>{
setEditorVisble(false)
}
const saveDocumentClick = async (docID?: string)=>{
const editDocumentClick = () => {
setEditorVisble(true);
};
const editCancelClick = () => {
setEditorVisble(false);
};
const saveDocumentClick = async (docID?: string) => {
const value = editorRef.current?.getValue();
let source = {}
let source = {};
try {
source = JSON.parse(value)
source = JSON.parse(value);
} catch (error) {
message.error('wrong json format')
return
message.error("wrong json format");
return;
}
let params = {
_index: row._index,
@ -57,18 +71,18 @@ export function Detail({
_type: row._type,
_source: source,
};
docID && (params['is_new'] = '1')
const res = await document.saveDocument(params)
if(!res.error) setEditorVisble(false)
}
const deleteDocumentClick = ()=>{
docID && (params["is_new"] = "1");
const res = await document.saveDocument(params);
if (!res.error) setEditorVisble(false);
};
const deleteDocumentClick = () => {
document.deleteDocument({
_index: row._index,
_id: row._id,
_type: row._type,
})
}
});
};
const menu = (
<Menu>
@ -76,72 +90,117 @@ export function Detail({
<a> Edit </a>
</Menu.Item>
<Menu.Item key="Delete">
<Popconfirm title="sure to delete" onConfirm={()=>{
deleteDocumentClick();
}}><a> Delete </a></Popconfirm>
<Popconfirm
title="sure to delete"
onConfirm={() => {
deleteDocumentClick();
}}
>
<a> Delete </a>
</Popconfirm>
</Menu.Item>
</Menu>
);
return (
<td colSpan={ columns.length + 2 }>
<div className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--justifyContentSpaceBetween">
<div className="euiFlexItem euiFlexItem--flexGrowZero euiText euiText--small">
<div className="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--directionRow">
<div className="euiFlexItem euiFlexItem--flexGrowZero">
<EuiIcon type="folderOpen" size="m" style={{marginTop: 5}}/>
</div>
<div className="euiFlexItem euiFlexItem--flexGrowZero">
<h4
data-test-subj="docTableRowDetailsTitle"
className="euiTitle euiTitle--xsmall"
>Expanded document</h4>
</div>
</div>
</div>
<div className="euiFlexItem euiFlexItem--flexGrowZero euiText euiText--small">
<div className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow">
<Drawer title="Edit document" visible={editorVisible} width="640" destroyOnClose={true}
onClose={()=>{setEditorVisble(false)}}>
<Descriptions>
<Descriptions.Item label="_index">{row._index}</Descriptions.Item>
<Descriptions.Item label="_id">{row._id}</Descriptions.Item>
</Descriptions>
<Editor
height="70vh"
theme="vs-light"
language="json"
options={{
minimap: {
enabled: false,
},
tabSize: 2,
wordBasedSuggestions: true,
}}
value={JSON.stringify(row._source, null, 2)}
onMount={handleEditorDidMount}
/>
<div style={{display:'flex', height: '10vh', alignItems:'center', justifyContent:'center'}}>
<div style={{marginLeft:'auto'}} >
<Button onClick={editCancelClick} style={{marginRight:5}}>Cancel</Button>
{/* <Button type="primary" onClick={()=>{}} style={{marginRight:5}}>Save as New</Button> */}
<SaveAsNewButton docID={row._id} saveDocumentClick={saveDocumentClick}/>
<Button type="primary" onClick={()=>{saveDocumentClick()}} >Save</Button>
<td
colSpan={columns.length + 2}
style={{
wordBreak: "break-all",
wordWrap: "break-word",
whiteSpace: "pre-wrap",
}}
>
<div className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--justifyContentSpaceBetween">
<div className="euiFlexItem euiFlexItem--flexGrowZero euiText euiText--small">
<div className="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--directionRow">
<div className="euiFlexItem euiFlexItem--flexGrowZero">
<EuiIcon type="folderOpen" size="m" style={{ marginTop: 5 }} />
</div>
<div className="euiFlexItem euiFlexItem--flexGrowZero">
<h4
data-test-subj="docTableRowDetailsTitle"
className="euiTitle euiTitle--xsmall"
>
Expanded document
</h4>
</div>
</div>
</Drawer>
</div>
<div className="euiFlexItem euiFlexItem--flexGrowZero euiText euiText--small">
{/* <a
<div className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow">
<Drawer
title="Edit document"
visible={editorVisible}
width="640"
destroyOnClose={true}
onClose={() => {
setEditorVisble(false);
}}
>
<Descriptions>
<Descriptions.Item label="_index">
{row._index}
</Descriptions.Item>
<Descriptions.Item label="_id">{row._id}</Descriptions.Item>
</Descriptions>
<Editor
height="70vh"
theme="vs-light"
language="json"
options={{
minimap: {
enabled: false,
},
tabSize: 2,
wordBasedSuggestions: true,
}}
value={JSON.stringify(row._source, null, 2)}
onMount={handleEditorDidMount}
/>
<div
style={{
display: "flex",
height: "10vh",
alignItems: "center",
justifyContent: "center",
}}
>
<div style={{ marginLeft: "auto" }}>
<Button onClick={editCancelClick} style={{ marginRight: 5 }}>
Cancel
</Button>
{/* <Button type="primary" onClick={()=>{}} style={{marginRight:5}}>Save as New</Button> */}
<SaveAsNewButton
docID={row._id}
saveDocumentClick={saveDocumentClick}
/>
<Button
type="primary"
onClick={() => {
saveDocumentClick();
}}
>
Save
</Button>
</div>
</div>
</Drawer>
<div className="euiFlexItem euiFlexItem--flexGrowZero euiText euiText--small">
{/* <a
className="euiLink"
onClick={()=>{setEditorVisble(true)}}
>Edit document</a> */}
<Dropdown overlay={menu} >
<a className="ant-dropdown-link" onClick={e => e.preventDefault()}>
Operation <Icon type="down" />
</a>
</Dropdown>
</div>
{/* <div className="euiFlexItem euiFlexItem--flexGrowZero euiText euiText--small">
<Dropdown overlay={menu}>
<a
className="ant-dropdown-link"
onClick={(e) => e.preventDefault()}
>
Operation <Icon type="down" />
</a>
</Dropdown>
</div>
{/* <div className="euiFlexItem euiFlexItem--flexGrowZero euiText euiText--small">
<a
className="euiLink"
>View surrounding documents</a>
@ -151,45 +210,58 @@ export function Detail({
className="euiLink"
>View single document</a>
</div> */}
</div>
</div>
</div>
</div>
</div>
<div data-test-subj="docViewer">
<DocViewer
columns={columns}
filter={onFilter}
hit={row}
indexPattern={indexPattern}
onAddColumn={onAddColumn}
onRemoveColumn={onRemoveColumn}
/>
</div>
</td>
)
<div data-test-subj="docViewer">
<DocViewer
columns={columns}
filter={onFilter}
hit={row}
indexPattern={indexPattern}
onAddColumn={onAddColumn}
onRemoveColumn={onRemoveColumn}
/>
</div>
</td>
);
}
const SaveAsNewButton = ({docID, saveDocumentClick}:any)=>{
const SaveAsNewButton = ({ docID, saveDocumentClick }: any) => {
const newID = generateNewID(docID);
const [newDocID, setNewDocID] = useState(newID)
const content = (<div style={{width: 200}}>
<div><Input value={newDocID} onChange={(e)=>{
setNewDocID(e.target.value)
}} /></div>
<div style={{marginTop:10}}><Button onClick={()=>{
saveDocumentClick(newDocID)
}}></Button></div>
</div>)
const [newDocID, setNewDocID] = useState(newID);
const content = (
<div style={{ width: 200 }}>
<div>
<Input
value={newDocID}
onChange={(e) => {
setNewDocID(e.target.value);
}}
/>
</div>
<div style={{ marginTop: 10 }}>
<Button
onClick={() => {
saveDocumentClick(newDocID);
}}
>
</Button>
</div>
</div>
);
return (
<Popover
content={content}
title="Please input new ID"
trigger="click"
// visible={this.state.visible}
// onVisibleChange={this.handleVisibleChange}
>
<Button style={{marginRight:5}} type="primary">Save as new</Button>
</Popover>
)
}
content={content}
title="Please input new ID"
trigger="click"
// visible={this.state.visible}
// onVisibleChange={this.handleVisibleChange}
>
<Button style={{ marginRight: 5 }} type="primary">
Save as new
</Button>
</Popover>
);
};

View File

@ -1,7 +1,9 @@
import {Open} from './open';
import {Cell} from './cell';
import {Detail} from './detail';
import {useState} from 'react';
import { Open } from "./open";
import { Cell } from "./cell";
import { Detail } from "./detail";
import React, { useState } from "react";
const MemoDetail = React.memo(Detail);
interface Props {
// sorting="sorting"
@ -25,62 +27,89 @@ export function TableRow({
onAddColumn,
onRemoveColumn,
row,
document
}:Props){
document,
}: Props) {
const mapping = indexPattern.fields.getByName;
const [open,setOpen] = useState(false);
const [open, setOpen] = useState(false);
return (
<>
<tr
className="kbnDocTable__row"
>
<Open open={open} onClick={()=>{
setOpen(!open);
}}/>
{(indexPattern.timeFieldName && !hideTimeColumn)? <Cell timefield={true}
row={row}
indexPattern={indexPattern}
inlineFilter={onFilter}
formatted={_displayField(indexPattern, row, indexPattern.timeFieldName)}
filterable={mapping(indexPattern.timeFieldName).filterable} //&& $scope.filter
column= {indexPattern.timeFieldName}/>: null}
<tr className="kbnDocTable__row">
<Open
open={open}
onClick={() => {
setOpen(!open);
}}
/>
{indexPattern.timeFieldName && !hideTimeColumn ? (
<Cell
timefield={true}
row={row}
indexPattern={indexPattern}
inlineFilter={onFilter}
formatted={_displayField(
indexPattern,
row,
indexPattern.timeFieldName
)}
filterable={mapping(indexPattern.timeFieldName).filterable} //&& $scope.filter
column={indexPattern.timeFieldName}
/>
) : null}
{columns.map(function (column: any) {
const isFilterable = mapping(column) && mapping(column).filterable ;//&& $scope.filter;
return <Cell key={'discover-cell-'+column} timefield={false}
row={row}
inlineFilter={onFilter}
indexPattern={indexPattern}
sourcefield={column === '_source'}
formatted={_displayField(indexPattern, row, column, true)}
filterable={isFilterable} //&& $scope.filter
column= {column}/>
{columns.map(function(column: any) {
const isFilterable = mapping(column) && mapping(column).filterable; //&& $scope.filter;
return (
<Cell
key={"discover-cell-" + column}
timefield={false}
row={row}
inlineFilter={onFilter}
indexPattern={indexPattern}
sourcefield={column === "_source"}
formatted={_displayField(indexPattern, row, column, true)}
filterable={isFilterable} //&& $scope.filter
column={column}
/>
);
})}
</tr>
{open? <tr className="kbnDocTableDetails__row">
<Detail columns={columns}
indexPattern={indexPattern}
row={row}
document={document}
onFilter={onFilter}
onAddColumn={onAddColumn}
onRemoveColumn={onRemoveColumn}/>
</tr>:null
}
</tr>
{open && (
<tr className="kbnDocTableDetails__row">
<MemoDetail
columns={columns}
indexPattern={indexPattern}
row={row}
document={document}
onFilter={onFilter}
onAddColumn={onAddColumn}
onRemoveColumn={onRemoveColumn}
/>
</tr>
)}
</>
)
);
}
const MIN_LINE_LENGTH = 20;
function _displayField(indexPattern:any, row: any, fieldName: string, truncate = false) {
function _displayField(
indexPattern: any,
row: any,
fieldName: string,
truncate = false
) {
const text = indexPattern.formatField(row, fieldName);
if (truncate && text.length > MIN_LINE_LENGTH) {
return <div className="truncate-by-height" dangerouslySetInnerHTML={{ __html: text }} >
</div>
return (
<div
className="truncate-by-height"
dangerouslySetInnerHTML={{ __html: text }}
></div>
);
}
return <span dangerouslySetInnerHTML={{ __html: text }} />;
}
return <span dangerouslySetInnerHTML={{ __html: text }} />;
}

View File

@ -16,12 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
import './doc_viewer.scss';
import React from 'react';
import { EuiTabbedContent } from '@elastic/eui';
import { getDocViewsRegistry } from '../../../kibana_services';
import { DocViewerTab } from './doc_viewer_tab';
import { DocView, DocViewRenderProps } from '../../doc_views/doc_views_types';
import "./doc_viewer.scss";
import React from "react";
import { EuiTabbedContent } from "@elastic/eui";
import { getDocViewsRegistry } from "../../../kibana_services";
import { DocViewerTab } from "./doc_viewer_tab";
import { DocView, DocViewRenderProps } from "../../doc_views/doc_views_types";
import TableContext from "../discover_table/table_context";
/**
* Rendering tabs with different views of 1 Elasticsearch hit in Discover.
@ -54,9 +55,23 @@ export function DocViewer(renderProps: DocViewRenderProps) {
// This condition takes care of unit tests with 0 tabs.
return null;
}
const { tableRef } = React.useContext(TableContext);
const [viewerWidth, setViewerWidth] = React.useState(
tableRef?.offsetWidth - 40
);
React.useEffect(() => {
const onResize = () => {
setViewerWidth(tableRef?.offsetWidth - 40);
};
window.addEventListener("resize", onResize);
return () => {
window.removeEventListener("resize", onResize);
};
}, []);
return (
<div className="kbnDocViewer">
<div className="kbnDocViewer" style={{ width: viewerWidth }}>
<EuiTabbedContent tabs={tabs} />
</div>
);

View File

@ -1,6 +1,6 @@
import moment, { unitOfTime } from 'moment-timezone';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import moment, { unitOfTime } from "moment-timezone";
import React, { Component } from "react";
import PropTypes from "prop-types";
import {
Axis,
@ -15,28 +15,27 @@ import {
BrushEndListener,
Theme,
LIGHT_THEME,
} from '@elastic/charts';
import lightEuiTheme from '@elastic/eui/dist/eui_theme_light.json';
import darkEuiTheme from '@elastic/eui/dist/eui_theme_dark.json';
} from "@elastic/charts";
import lightEuiTheme from "@elastic/eui/dist/eui_theme_light.json";
import darkEuiTheme from "@elastic/eui/dist/eui_theme_dark.json";
import "@elastic/charts/dist/theme_light.css";
import { Subscription, combineLatest } from 'rxjs';
import {CurrentTime} from './current_time';
import { Subscription, combineLatest } from "rxjs";
import { CurrentTime } from "./current_time";
import {
Endzones,
getAdjustedInterval,
renderEndzoneTooltip,
} from './endzones';
} from "./endzones";
function getTimezone() {
const detectedTimezone = moment.tz.guess();
if (detectedTimezone) return detectedTimezone;
else return moment().format('Z');
else return moment().format("Z");
}
export class DiscoverHistogram extends Component{
static propTypes = {
export class DiscoverHistogram extends Component {
static propTypes = {
chartData: PropTypes.object,
timefilterUpdateHandler: PropTypes.func,
};
@ -62,7 +61,7 @@ export class DiscoverHistogram extends Component{
}
}
onBrushEnd = ({ x }) => {
onBrushEnd = ({ x }) => {
if (!x) {
return;
}
@ -71,7 +70,7 @@ export class DiscoverHistogram extends Component{
};
onElementClick = (xInterval) => ([elementData]) => {
const startRange = (elementData)[0].x;
const startRange = elementData[0].x;
const range = {
from: startRange,
@ -130,28 +129,33 @@ export class DiscoverHistogram extends Component{
),
};
const tooltipProps = {
headerFormatter: renderEndzoneTooltip(xInterval, domainStart, domainEnd, this.formatXValue),
headerFormatter: renderEndzoneTooltip(
xInterval,
domainStart,
domainEnd,
this.formatXValue
),
type: TooltipType.VerticalCursor,
};
// const xAxisFormatter = getServices().data.fieldFormats.deserialize(
// this.props.chartData.yAxisFormat
// );
const xAxisFormatter = {
convert: (value)=>{
convert: (value) => {
return value;
}
}
},
};
//console.log(data)
return (
<Chart size="100%" size={{height:200}}>
<Chart size="100%" size={{ height: 120 }}>
<Settings
xDomain={xDomain}
onBrushEnd={this.onBrushEnd}
onElementClick={this.onElementClick(xInterval)}
tooltip={tooltipProps}
theme={LIGHT_THEME}
// baseTheme={chartsBaseTheme}
// baseTheme={chartsBaseTheme}
/>
<Axis
id="discover-histogram-left-axis"
@ -159,13 +163,15 @@ export class DiscoverHistogram extends Component{
ticks={5}
title={chartData.yAxisLabel}
integersOnly
tickFormat={(value) => {return xAxisFormatter.convert(value)}}
tickFormat={(value) => {
return xAxisFormatter.convert(value);
}}
showGridLines
/>
<Axis
id="discover-histogram-bottom-axis"
position={Position.Bottom}
title={chartData.xAxisLabel}
// title={chartData.xAxisLabel}
tickFormat={this.formatXValue}
ticks={10}
//showGridLines
@ -185,12 +191,11 @@ export class DiscoverHistogram extends Component{
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor="x"
yAccessors={['y']}
yAccessors={["y"]}
data={data}
timeZone={timeZone}
name={chartData.yAxisLabel}
/>
</Chart>
);
}

View File

@ -16,12 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { EuiCodeBlock } from '@elastic/eui';
import { DocViewRenderProps } from '../../doc_views/doc_views_types';
import React from "react";
import { EuiCodeBlock } from "@elastic/eui";
import { DocViewRenderProps } from "../../doc_views/doc_views_types";
export function JsonCodeBlock({ hit }: DocViewRenderProps) {
const label = 'Read only JSON view of an elasticsearch document';
const label = "Read only JSON view of an elasticsearch document";
return (
<EuiCodeBlock aria-label={label} language="json" isCopyable paddingSize="s">
{JSON.stringify(hit, null, 2)}

View File

@ -24,29 +24,34 @@ import {
EuiPopoverTitle,
EuiSelectable,
EuiButtonEmptyProps,
EuiTabs,
EuiTabbedContent,
EuiTab,
EuiSwitch,
} from '@elastic/eui';
import { EuiSelectableProps } from '@elastic/eui/src/components/selectable/selectable';
import { IndexPatternRef } from './types';
export type ChangeIndexPatternTriggerProps = EuiButtonEmptyProps & {
label: string;
title?: string;
};
// TODO: refactor to shared component with ../../../../../../../../x-pack/legacy/plugins/lens/public/indexpattern_plugin/change_indexpattern
export function ChangeIndexPattern({
indexPatternRefs,
indexPatternId,
onChangeIndexPattern,
trigger,
selectableProps,
indices,
}: {
trigger: ChangeIndexPatternTriggerProps;
indexPatternRefs: IndexPatternRef[];
onChangeIndexPattern: (newId: string) => void;
onChangeIndexPattern: (newId: string, typ: string) => void;
indexPatternId?: string;
selectableProps?: EuiSelectableProps;
indices: string[];
}) {
const [isPopoverOpen, setPopoverIsOpen] = useState(false);
@ -68,22 +73,21 @@ export function ChangeIndexPattern({
);
};
return (
<EuiPopover
button={createTrigger()}
isOpen={isPopoverOpen}
closePopover={() => setPopoverIsOpen(false)}
className="eui-textTruncate"
anchorClassName="eui-textTruncate"
display="block"
panelPaddingSize="s"
ownFocus
>
<div style={{ width: 320 }}>
<EuiPopoverTitle>
</EuiPopoverTitle>
<EuiSelectable
const [selectedTabId, setSelectedTabId] = useState(indices.includes(indexPatternId) ? 1 :0);
const onSelectedTabChanged = (id: number) => {
setSelectedTabId(id);
};
const [includeSystemIndex, setIncludeSystemIndex] = useState(false);
const tabs = React.useMemo(()=>{
const showIndices = includeSystemIndex ? indices: indices.filter(key=>!key.startsWith("."));
const tabs = [
{
id: 'view',
name: 'View',
disabled: false,
content: ( <EuiSelectable
style={{marginTop:10}}
data-test-subj="indexPattern-switcher"
{...selectableProps}
searchable
@ -98,7 +102,7 @@ export function ChangeIndexPattern({
const choice = (choices.find(({ checked }) => checked) as unknown) as {
value: string;
};
onChangeIndexPattern(choice.value);
onChangeIndexPattern(choice.value, 'view');
setPopoverIsOpen(false);
}}
searchProps={{
@ -112,7 +116,103 @@ export function ChangeIndexPattern({
{list}
</>
)}
</EuiSelectable>
</EuiSelectable>),
},
{
id: 'index',
name: 'Index',
disabled: false,
content:(
<div>
<div style={{display:'flex', margin:'10px auto', flexDirection: 'row-reverse',}}>
<EuiSwitch
label="Include system index"
checked={includeSystemIndex}
onChange={(e) => setIncludeSystemIndex(!includeSystemIndex)}
/>
</div>
<EuiSelectable
style={{marginTop:5}}
{...selectableProps}
searchable
singleSelection="always"
options={showIndices.map((indexName) => ({
label: indexName,
key: indexName,
value: indexName,
checked: indexName === indexPatternId ? 'on' : undefined,
}))}
onChange={(choices) => {
const choice = (choices.find(({ checked }) => checked) as unknown) as {
value: string;
};
onChangeIndexPattern(choice.value, 'index');
setPopoverIsOpen(false);
}}
searchProps={{
compressed: true,
...(selectableProps ? selectableProps.searchProps : undefined),
}}
>
{(list, search) => (
<>
{search}
{list}
</>
)}
</EuiSelectable></div>),
},
];
return tabs;
},[selectableProps, indexPatternId, indexPatternRefs, indices, includeSystemIndex])
const selectedTabContent = React.useMemo(() => {
return tabs.find((obj) => obj.id === selectedTabId)?.content;
}, [selectedTabId, tabs]);
const renderTabs = () => {
return tabs.map((tab, index) => (
<EuiTab
key={index}
onClick={() => onSelectedTabChanged(tab.id)}
isSelected={tab.id === selectedTabId}
disabled={tab.disabled}
>
{tab.name}
</EuiTab>
));
};
return (
<EuiPopover
button={createTrigger()}
isOpen={isPopoverOpen}
closePopover={() => setPopoverIsOpen(false)}
className="eui-textTruncate"
anchorClassName="eui-textTruncate"
display="block"
panelPaddingSize="s"
ownFocus
>
<div style={{ width: 320 }}>
{/* <EuiPopoverTitle>
</EuiPopoverTitle> */}
{/* <EuiTabs size="s" expand>
{renderTabs()}
</EuiTabs> */}
<EuiTabbedContent
tabs={tabs}
initialSelectedTab={tabs[selectedTabId]}
autoFocus="selected"
onTabClick={(tab) => {
const idx = tabs.findIndex(item=>item.id == tab.id);
setSelectedTabId(idx);
}}
/>
{/* <div style={{marginTop:5}}></div>
{selectedTabContent} */}
</div>
</EuiPopover>
);

View File

@ -16,15 +16,20 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useState } from 'react';
import { EuiPopover, EuiPopoverTitle, EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import { DiscoverFieldDetails } from './discover_field_details';
import { FieldIcon, FieldButton } from '../../../../../kibana_react/public';
import { FieldDetails } from './types';
import { IndexPatternField, IndexPattern } from '../../../../../data/public';
import { shortenDottedString } from '../../helpers';
import { getFieldTypeName } from './lib/get_field_type_name';
import './discover_field.scss';
import React, { useState } from "react";
import {
EuiPopover,
EuiPopoverTitle,
EuiButtonIcon,
EuiToolTip,
} from "@elastic/eui";
import { DiscoverFieldDetails } from "./discover_field_details";
import { FieldIcon, FieldButton } from "../../../../../kibana_react/public";
import { FieldDetails } from "./types";
import { IndexPatternField, IndexPattern } from "../../../../../data/public";
import { shortenDottedString } from "../../helpers";
import { getFieldTypeName } from "./lib/get_field_type_name";
import "./discover_field.scss";
export interface DiscoverFieldProps {
/**
@ -42,7 +47,11 @@ export interface DiscoverFieldProps {
/**
* Callback to add a filter to filter bar
*/
onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
onAddFilter: (
field: IndexPatternField | string,
value: string,
type: "+" | "-"
) => void;
/**
* Callback to remove/deselect a the field
* @param fieldName
@ -72,8 +81,8 @@ export function DiscoverField({
selected,
useShortDots,
}: DiscoverFieldProps) {
const addLabelAria = `Add ${field.name } to table`;
const removeLabelAria = `Remove ${field.name } to table`;
const addLabelAria = `Add ${field.name} to table`;
const removeLabelAria = `Remove ${field.name} to table`;
const [infoIsOpen, setOpen] = useState(false);
@ -93,11 +102,15 @@ export function DiscoverField({
// u200B is a non-width white-space character, which allows
// the browser to efficiently word-wrap right after the dot
// without us having to draw a lot of extra DOM elements, etc
return str ? str.replace(/\./g, '.\u200B') : '';
return str ? str.replace(/\./g, ".\u200B") : "";
}
const dscFieldIcon = (
<FieldIcon type={field.type} label={getFieldTypeName(field.type)} scripted={field.scripted} />
<FieldIcon
type={field.type}
label={getFieldTypeName(field.type)}
scripted={field.scripted}
/>
);
const fieldName = (
@ -106,22 +119,21 @@ export function DiscoverField({
title={field.name}
className="dscSidebarField__name"
>
{useShortDots ? wrapOnDot(shortenDottedString(field.name)) : wrapOnDot(field.displayName)}
{useShortDots
? wrapOnDot(shortenDottedString(field.name))
: wrapOnDot(field.displayName)}
</span>
);
let actionButton;
if (field.name !== '_source' && !selected) {
if (field.name !== "_source" && !selected) {
actionButton = (
<EuiToolTip
delay="long"
content={ 'Add field as column'}
>
<EuiToolTip delay="long" content={"Add field as column"}>
<EuiButtonIcon
iconType="plusInCircleFilled"
className="dscSidebarItem__action"
onClick={(ev: React.MouseEvent<HTMLButtonElement>) => {
if (ev.type === 'click') {
if (ev.type === "click") {
ev.currentTarget.focus();
}
ev.preventDefault();
@ -133,18 +145,15 @@ export function DiscoverField({
/>
</EuiToolTip>
);
} else if (field.name !== '_source' && selected) {
} else if (field.name !== "_source" && selected) {
actionButton = (
<EuiToolTip
delay="long"
content={ 'Remove field from table'}
>
<EuiToolTip delay="long" content={"Remove field from table"}>
<EuiButtonIcon
color="danger"
iconType="cross"
className="dscSidebarItem__action"
onClick={(ev: React.MouseEvent<HTMLButtonElement>) => {
if (ev.type === 'click') {
if (ev.type === "click") {
ev.currentTarget.focus();
}
ev.preventDefault();
@ -158,7 +167,7 @@ export function DiscoverField({
);
}
if (field.type === '_source') {
if (field.type === "_source") {
return (
<FieldButton
size="s"
@ -194,10 +203,7 @@ export function DiscoverField({
anchorPosition="rightUp"
panelClassName="dscSidebarItem__fieldPopoverPanel"
>
<EuiPopoverTitle>
{' '}
{ 'Top 5 values'}
</EuiPopoverTitle>
<EuiPopoverTitle> {"Top 5 values"}</EuiPopoverTitle>
{infoIsOpen && (
<DiscoverFieldDetails
indexPattern={indexPattern}

View File

@ -34,7 +34,8 @@ export interface DiscoverIndexPatternProps {
/**
* triggered when user selects a new index pattern
*/
setIndexPattern: (id: string) => void;
setIndexPattern: (id: string, typ: string) => void;
indices: string[];
}
/**
@ -44,6 +45,7 @@ export function DiscoverIndexPattern({
indexPatternList,
selectedIndexPattern,
setIndexPattern,
indices,
}: DiscoverIndexPatternProps) {
const options: IndexPatternRef[] = (indexPatternList || []).map((entity) => ({
id: entity.id,
@ -75,13 +77,27 @@ export function DiscoverIndexPattern({
}}
indexPatternId={selected.id}
indexPatternRefs={options}
onChangeIndexPattern={(id) => {
const indexPattern = options.find((pattern) => pattern.id === id);
onChangeIndexPattern={(id, typ) => {
let indexPattern = null;
if(typ == 'index'){
indices.forEach((indexName)=>{
if(indexName == id){
indexPattern = {
id: indexName,
title: indexName,
viewName: indexName,
}
}
})
}else{
indexPattern = options.find((pattern) => pattern.id === id);
}
if (indexPattern) {
setIndexPattern(id);
setIndexPattern(id, typ);
setSelected(indexPattern);
}
}}
indices={indices}
/>
</div>
);

View File

@ -1,4 +1,4 @@
@import '../../../../../core/public/variables.scss';
@import "../../../../../core/public/variables.scss";
.dscSidebar__container {
padding-left: 0 !important;
padding-right: 0 !important;
@ -26,11 +26,17 @@
.dscFieldListHeader {
padding: $euiSizeS $euiSizeS 0 $euiSizeS;
background-color: lightOrDarkTheme(tint($euiColorPrimary, 90%), $euiColorLightShade);
background-color: lightOrDarkTheme(
tint($euiColorPrimary, 90%),
$euiColorLightShade
);
}
.dscFieldList--popular {
background-color: lightOrDarkTheme(tint($euiColorPrimary, 90%), $euiColorLightShade);
background-color: lightOrDarkTheme(
tint($euiColorPrimary, 90%),
$euiColorLightShade
);
}
.dscFieldChooser {
@ -45,7 +51,7 @@
.dscSidebarItem {
&:hover,
&:focus-within,
&[class*='-isActive'] {
&[class*="-isActive"] {
.dscSidebarItem__action {
opacity: 1;
}
@ -95,3 +101,23 @@
color: $euiTextColor;
margin-bottom: $euiSizeS;
}
#fields-tree-wrapper {
.ant-tree-switcher-noop {
display: none;
}
.ant-tree-treenode-switcher-close {
padding: 0;
.ant-tree-node-content-wrapper {
height: 32px;
}
.ant-tree-title {
display: block;
height: 32px;
.kbnFieldButton__button {
padding: 0;
align-items: center;
}
}
}
}

View File

@ -16,22 +16,27 @@
* specific language governing permissions and limitations
* under the License.
*/
import './discover_sidebar.scss';
import React, { useCallback, useEffect, useState, useMemo } from 'react';
import { EuiButtonIcon, EuiTitle, EuiSpacer,EuiHideFor } from '@elastic/eui';
import { sortBy } from 'lodash';
import { DiscoverField } from './discover_field';
import { DiscoverIndexPattern } from './discover_index_pattern';
import { DiscoverFieldSearch } from './discover_field_search';
import { IndexPatternAttributes } from '../../../../../data/common';
import { SavedObject } from '../../../../../../core/types';
import "./discover_sidebar.scss";
import React, { useCallback, useEffect, useState, useMemo } from "react";
import { EuiButtonIcon, EuiTitle, EuiSpacer, EuiHideFor } from "@elastic/eui";
import { sortBy } from "lodash";
import { DiscoverField } from "./discover_field";
import { DiscoverIndexPattern } from "./discover_index_pattern";
import { DiscoverFieldSearch } from "./discover_field_search";
import { IndexPatternAttributes } from "../../../../../data/common";
import { SavedObject } from "../../../../../../core/types";
// import { FIELDS_LIMIT_SETTING } from '../../../../common';
import { groupFields } from './lib/group_fields';
import { IndexPatternField, IndexPattern, UI_SETTINGS } from '../../../../../data/public';
import { getDetails } from './lib/get_details';
import { getDefaultFieldFilter, setFieldFilterProp } from './lib/field_filter';
import { getIndexPatternFieldList } from './lib/get_index_pattern_field_list';
import { groupFields } from "./lib/group_fields";
import {
IndexPatternField,
IndexPattern,
UI_SETTINGS,
} from "../../../../../data/public";
import { getDetails } from "./lib/get_details";
import { getDefaultFieldFilter, setFieldFilterProp } from "./lib/field_filter";
import { getIndexPatternFieldList } from "./lib/get_index_pattern_field_list";
// import { getServices } from '../../../kibana_services';
import { Tree, Icon } from "antd";
export interface DiscoverSidebarProps {
/**
@ -57,7 +62,11 @@ export interface DiscoverSidebarProps {
/**
* Callback function when adding a filter from sidebar
*/
onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
onAddFilter: (
field: IndexPatternField | string,
value: string,
type: "+" | "-"
) => void;
/**
* Callback function when removing a field
* @param fieldName
@ -72,6 +81,7 @@ export interface DiscoverSidebarProps {
*/
setIndexPattern: (id: string) => void;
isClosed: boolean;
indices: string[];
}
export function DiscoverSidebar({
@ -85,16 +95,22 @@ export function DiscoverSidebar({
selectedIndexPattern,
setIndexPattern,
isClosed,
indices,
}: DiscoverSidebarProps) {
const [showFields, setShowFields] = useState(false);
const [fields, setFields] = useState<IndexPatternField[] | null>(null);
const [fieldFilterState, setFieldFilterState] = useState(getDefaultFieldFilter());
const [fieldFilterState, setFieldFilterState] = useState(
getDefaultFieldFilter()
);
// const services = useMemo(() => getServices(), []);
useEffect(() => {
const newFields = getIndexPatternFieldList(selectedIndexPattern, fieldCounts);
const newFields = getIndexPatternFieldList(
selectedIndexPattern,
fieldCounts
);
setFields(newFields);
}, [selectedIndexPattern, fieldCounts, hits, ]);//services
}, [selectedIndexPattern, fieldCounts, hits]); //services
const onChangeFieldSearch = useCallback(
(field: string, value: string | boolean | undefined) => {
@ -105,27 +121,51 @@ export function DiscoverSidebar({
);
const getDetailsByField = useCallback(
(ipField: IndexPatternField) => getDetails(ipField, hits, columns, selectedIndexPattern),
(ipField: IndexPatternField) =>
getDetails(ipField, hits, columns, selectedIndexPattern),
[hits, columns, selectedIndexPattern]
);
const popularLimit = 5;//services.uiSettings.get(FIELDS_LIMIT_SETTING);
const useShortDots = false;//services.uiSettings.get(UI_SETTINGS.SHORT_DOTS_ENABLE);
const popularLimit = 5; //services.uiSettings.get(FIELDS_LIMIT_SETTING);
const useShortDots = false; //services.uiSettings.get(UI_SETTINGS.SHORT_DOTS_ENABLE);
const {
selected: selectedFields,
popular: popularFields,
unpopular: unpopularFields,
} = useMemo(() => groupFields(fields, columns, popularLimit, fieldCounts, fieldFilterState), [
fields,
columns,
popularLimit,
fieldCounts,
fieldFilterState,
]);
fieldsTree,
} = useMemo(() => {
const groupedFields = groupFields(
fields,
columns,
popularLimit,
fieldCounts,
fieldFilterState
);
const fieldsTree = {};
groupedFields.unpopular.forEach((field) => {
const keys = field.displayName.split(".");
let currentObj = fieldsTree;
keys.forEach((key: string, i: number) => {
if (!currentObj[key]) {
currentObj[key] = {};
}
if (keys.length == i + 1) {
field.isLeaf = true;
currentObj[key] = field;
return;
}
currentObj = currentObj[key];
});
});
return {
...groupedFields,
fieldsTree,
};
}, [fields, columns, popularLimit, fieldCounts, fieldFilterState]);
const fieldTypes = useMemo(() => {
const result = ['any'];
const result = ["any"];
if (Array.isArray(fields)) {
for (const field of fields) {
if (result.indexOf(field.type) === -1) {
@ -136,20 +176,56 @@ export function DiscoverSidebar({
return result;
}, [fields]);
if (!selectedIndexPattern || !fields || isClosed ) {
if (!selectedIndexPattern || !fields || isClosed) {
return null;
}
const buildTree = (treeObj: any) => {
return Object.keys(treeObj).map((key) => {
if (treeObj[key].isLeaf) {
return (
<Tree.TreeNode
icon={<Icon type="carry-out" />}
selectable={false}
title={
<DiscoverField
field={treeObj[key]}
indexPattern={selectedIndexPattern}
onAddField={onAddField}
onRemoveField={onRemoveField}
onAddFilter={onAddFilter}
getDetails={getDetailsByField}
useShortDots={true}
/>
}
key={key}
/>
);
}
return (
<Tree.TreeNode
icon={<Icon type="carry-out" />}
selectable={false}
title={key}
key={key}
>
{buildTree(treeObj[key])}
</Tree.TreeNode>
);
});
};
return (
<EuiHideFor sizes={['xs', 's']}>
<section
className="sidebar-list"
aria-label={'Index and fields'}
>
<EuiHideFor sizes={["xs", "s"]}>
<section className="sidebar-list" aria-label={"Index and fields"}>
<DiscoverIndexPattern
selectedIndexPattern={selectedIndexPattern}
setIndexPattern={setIndexPattern}
indexPatternList={sortBy(indexPatternList, (o) => o.attributes.viewName)}
indexPatternList={sortBy(
indexPatternList,
(o) => o.attributes.viewName
)}
indices={indices}
/>
<div className="dscSidebar__item">
<form>
@ -164,9 +240,7 @@ export function DiscoverSidebar({
{fields.length > 0 && (
<>
<EuiTitle size="xxxs" id="selected_fields">
<h3>
Selected fields
</h3>
<h3>Selected fields</h3>
</EuiTitle>
<EuiSpacer size="xs" />
<ul
@ -196,10 +270,12 @@ export function DiscoverSidebar({
})}
</ul>
<div className="euiFlexGroup euiFlexGroup--gutterMedium">
<EuiTitle size="xxxs" id="available_fields" className="euiFlexItem">
<h3>
Available fields
</h3>
<EuiTitle
size="xxxs"
id="available_fields"
className="euiFlexItem"
>
<h3>Available fields</h3>
</EuiTitle>
{/* <div className="euiFlexItem euiFlexItem--flexGrowZero">
<EuiButtonIcon
@ -220,15 +296,20 @@ export function DiscoverSidebar({
<div>
<EuiTitle
size="xxxs"
className={`dscFieldListHeader ${!showFields ? 'hidden-sm hidden-xs' : ''}`}
className={`dscFieldListHeader ${
!showFields ? "hidden-sm hidden-xs" : ""
}`}
>
<h4 style={{ fontWeight: 'normal' }} id="available_fields_popular">
<h4
style={{ fontWeight: "normal" }}
id="available_fields_popular"
>
Popular
</h4>
</EuiTitle>
<ul
className={`dscFieldList dscFieldList--popular ${
!showFields ? 'hidden-sm hidden-xs' : ''
!showFields ? "hidden-sm hidden-xs" : ""
}`}
aria-labelledby="available_fields available_fields_popular"
data-test-subj={`fieldList-popular`}
@ -256,9 +337,9 @@ export function DiscoverSidebar({
</div>
)}
<ul
{/* <ul
className={`dscFieldList dscFieldList--unpopular ${
!showFields ? 'hidden-sm hidden-xs' : ''
!showFields ? "hidden-sm hidden-xs" : ""
}`}
aria-labelledby="available_fields"
data-test-subj={`fieldList-unpopular`}
@ -282,9 +363,18 @@ export function DiscoverSidebar({
</li>
);
})}
</ul>
</ul> */}
<div id="fields-tree-wrapper">
<Tree
showLine={false}
showIcon={false}
defaultExpandedKeys={["0-0-0", "0-0-1", "0-0-2"]}
>
{buildTree(fieldsTree)}
</Tree>
</div>
</div>
</section>
</EuiHideFor>
</EuiHideFor>
);
}

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback } from "react";
import {
EuiFlexGroup,
EuiFlexItem,
@ -24,8 +24,8 @@ import {
EuiText,
EuiSelect,
EuiIconTip,
} from '@elastic/eui';
import moment from 'moment';
} from "@elastic/eui";
import moment from "moment";
export interface TimechartHeaderProps {
/**
@ -39,6 +39,7 @@ export interface TimechartHeaderProps {
scaled?: boolean;
description?: string;
scale?: number;
timeFieldName: string;
};
/**
* Range of dates to be displayed
@ -59,6 +60,7 @@ export interface TimechartHeaderProps {
* selected interval
*/
stateInterval: string;
hits: number;
}
export function TimechartHeader({
@ -68,12 +70,13 @@ export function TimechartHeader({
options,
onChangeInterval,
stateInterval,
hits,
}: TimechartHeaderProps) {
const [interval, setInterval] = useState(stateInterval);
const toMoment = useCallback(
(datetime: string) => {
if (!datetime) {
return '';
return "";
}
if (!dateFormat) {
return datetime;
@ -92,27 +95,36 @@ export function TimechartHeader({
onChangeInterval(e.target.value);
};
if (!timeRange || !bucketInterval) {
return null;
}
// if (!timeRange || !bucketInterval) {
// return null;
// }
return (
<EuiFlexGroup gutterSize="s" responsive justifyContent="center" alignItems="center">
<EuiFlexItem grow={false}>
<EuiToolTip
content={ 'To change the time, use the global time filter above'}
delay="long"
>
<EuiText data-test-subj="discoverIntervalDateRange" size="s">
{`${toMoment(timeRange.from)} - ${toMoment(timeRange.to)} ${
interval !== 'auto'
? 'per'
: ''
<EuiFlexGroup
gutterSize="s"
responsive
justifyContent="center"
alignItems="center"
>
<EuiFlexItem grow={false}>
<div>
Found <span style={{ fontSize: 16, fontWeight: "bold" }}>{hits}</span>{" "}
records
{timeRange && (
<span style={{ fontSize: 12, marginLeft: 5 }}>
{`between ${toMoment(timeRange.from)} and ${toMoment(
timeRange.to
)} ${
interval !== "auto" ? "" : "" //per
}`}
</EuiText>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
</span>
)}
{bucketInterval && (
<span>{`(${bucketInterval.timeFieldName} per ${bucketInterval.description})`}</span>
)}
</div>
</EuiFlexItem>
{/* <EuiFlexItem grow={false}>
<EuiSelect
aria-label={'Time interval'}
compressed
@ -145,7 +157,7 @@ export function TimechartHeader({
) : undefined
}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem> */}
</EuiFlexGroup>
);
}

View File

@ -23,4 +23,5 @@ const DOT_PREFIX_RE = /(.).+?\./g;
* Convert a dot.notated.string into a short
* version (d.n.string)
*/
export const shortenDottedString = (input: string) => input.replace(DOT_PREFIX_RE, '$1.');
export const shortenDottedString = (input: string) =>
input.replace(DOT_PREFIX_RE, ""); //'$1.');

View File

@ -0,0 +1,39 @@
import { useEffect, useRef } from "react";
export function useOnClickOutside(handler) {
const ref = useRef();
useEffect(
() => {
const listener = (event) => {
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
if (window.PointerEvent) {
document.addEventListener("pointerdown", listener);
} else {
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
}
return () => {
if (window.PointerEvent) {
document.removeEventListener("pointerdown", listener);
} else {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
}
};
},
// Add ref and handler to effect dependencies
// It's worth noting that because passed in handler is a new ...
// ... function on every render that will cause this effect ...
// ... callback/cleanup to run every render. It's not a big deal ...
// ... but to optimize you can wrap handler in useCallback before ...
// ... passing it into this hook.
[handler]
);
return [ref];
}

View File

@ -101,6 +101,7 @@ export default {
type: 'saveData',
payload: {
clusterList: newClusterList,
clusterTotal: res.total,
search: {
...search,
cluster: payload,

View File

@ -1,9 +1,13 @@
import * as React from 'react';
import {Tabs} from 'antd';
import {Tabs, Row, Col, Card} from 'antd';
import Clusters from './components/clusters';
import styles from "./Overview.less";
import {connect} from "dva";
import {formatter} from '@/lib/format';
const {TabPane} = Tabs;
const panes = [
{ title: 'Clusters', component: Clusters, key: 'clusters' },
{ title: 'Hosts', component: 'Content of Tab 2', key: 'hosts' },
@ -11,9 +15,66 @@ const panes = [
{title: 'Indices', component: 'Content of Tab 3',key: 'indices'},
];
const NewOverview = ()=>{
const NewOverview = (props)=>{
React.useEffect(()=>{
const {dispatch} = props;
dispatch({
type: 'clusterOverview/fetchOverview',
})
},[])
const {clusterTotal, overview} = props;
const totalStoreSize = formatter.bytes(overview?.total_store_size_in_bytes || 0);
return (<div style={{background:'#fff'}} className="overview">
<div>
<Row gutter={24} className={styles.rowSpace}>
<Col md={6} sm={12}>
<Card
bodyStyle={{ paddingBottom: 20 }}
className={styles.clusterMeta}
>
<Card.Meta title='集群总数' className={styles.title} />
<div>
<span className={styles.total}>{clusterTotal?.value}</span>
</div>
</Card>
</Col>
<Col md={6} sm={12}>
<Card
bodyStyle={{ paddingBottom: 20 }}
className={styles.clusterMeta}
>
<Card.Meta title='主机总数' className={styles.title} />
<div>
<span className={styles.total}>{overview?.total_host}</span>
</div>
</Card>
</Col>
<Col md={6} sm={12}>
<Card
bodyStyle={{ paddingBottom: 20 }}
className={styles.clusterMeta}
>
<Card.Meta title='节点总数' className={styles.title} />
<div>
<span className={styles.total}>{overview?.total_node}</span>
</div>
</Card>
</Col>
<Col md={6} sm={12}>
<Card
bodyStyle={{ paddingBottom: 20 }}
className={styles.clusterMeta}
>
<Card.Meta title='存储空间' className={styles.title} />
<div>
<span className={styles.total}>{totalStoreSize.size || '-'}</span><span className={styles.unit}>{totalStoreSize.unit}</span>
</div>
</Card>
</Col>
</Row>
</div>
<div>
<Tabs
onChange={()=>{}}
@ -31,4 +92,10 @@ const NewOverview = ()=>{
</div>);
}
export default NewOverview;
export default connect(({
clusterOverview,
global
})=>({
overview: clusterOverview.overview,
clusterTotal: global.clusterTotal,
}))(NewOverview)

File diff suppressed because it is too large Load Diff

View File

@ -135,12 +135,7 @@ class Index extends PureComponent {
title: '索引名称',
dataIndex: 'index',
render: (text, record) => (
<a onClick={()=>{
this.setState({
editingIndex: record,
drawerVisible: true,
});
}}>{text}</a>
<Link to={`/data/discover?viewID=${text}`}>{text}</Link>
)
},
{
@ -162,8 +157,13 @@ class Index extends PureComponent {
title: '操作',
render: (text, record) => (
<Fragment>
{/* <a onClick={() => this.handleUpdateModalVisible(true, record)}></a>
<Divider type="vertical" /> */}
<a onClick={() => {
this.setState({
editingIndex: record,
drawerVisible: true,
});
}}>设置</a>
<Divider type="vertical" />
<Popconfirm title="Sure to delete?" onConfirm={() => this.handleDeleteClick(record.index)}>
<a>删除</a>
</Popconfirm>

View File

@ -0,0 +1,98 @@
import { EuiToolTip, EuiSelect, EuiIconTip } from "@elastic/eui";
import { useOnClickOutside } from "@/lib/hooks/use_click_outsize";
import { useEffect, useState } from "react";
export const SettingContent = ({
onVisibleChange,
bucketInterval = {},
options = [],
onChangeInterval,
stateInterval,
indexPattern,
timeFields,
onTimeFieldChange,
}) => {
const [interval, setInterval] = useState(stateInterval);
useEffect(() => {
setInterval(stateInterval);
}, [stateInterval]);
const [settingRef] = useOnClickOutside(() => {
if (typeof onVisibleChange == "function") {
onVisibleChange(false);
}
});
const handleIntervalChange = (e) => {
setInterval(e.target.value);
onChangeInterval(e.target.value);
};
return (
<div className="dscSetting setting-content" ref={settingRef}>
<div className="setting-wrapper">
<div className="setting-row">
<span className="label">Time Field</span>
<div>
<EuiSelect
id="timefield"
hasNoInitialSelection={true}
value={indexPattern?.timeFieldName}
options={timeFields.map((tf) => {
return {
value: tf,
text: tf,
};
})}
onChange={(e) => {
onTimeFieldChange(indexPattern.id, e.target.value);
}}
/>
</div>
</div>
{indexPattern.timeFieldName ? (
<div className="setting-row">
<span className="label">Time Interval</span>
<div>
<EuiSelect
aria-label={"Time interval"}
compressed
id="dscResultsIntervalSelector"
options={options
.filter(({ val }) => val !== "custom")
.map(({ display, val }) => {
return {
text: display,
value: val,
label: display,
};
})}
value={interval}
onChange={handleIntervalChange}
append={
bucketInterval.scaled ? (
<EuiIconTip
id="discoverIntervalIconTip"
content={`This interval creates ${
bucketInterval?.scale && bucketInterval?.scale > 1
? "buckets that are too large"
: "too many buckets"
} to show in the selected time range, so it has been scaled to ${
bucketInterval.description
}.`}
color="warning"
size="s"
type="alert"
/>
) : (
undefined
)
}
/>
</div>
</div>
) : null}
</div>
</div>
);
};

View File

@ -21,17 +21,18 @@
.dscPageContent__wrapper {
padding: 0 5 5 0;
overflow: hidden; // Ensures horizontal scroll of table
box-shadow: 0 2px 2px -1px rgb(152 162 179 / 30%), 0 1px 5px -2px rgb(152 162 179 / 30%);
box-shadow: 0 2px 2px -1px rgb(152 162 179 / 30%),
0 1px 5px -2px rgb(152 162 179 / 30%);
// border: 1px solid #D3DAE6;
border-radius: 4px;
flex-grow: 1;
border-radius: 4px;
flex-grow: 1;
}
.dscPageContent {
//border: $euiBorderThin;
// text-align: center;
box-shadow: none !important;
.euiDataGrid--fullScreen{
.euiDataGrid--fullScreen {
z-index: 801;
}
position: relative;
@ -57,7 +58,7 @@
// SASSTODO: the visualizing component should have an option or a modifier
.series > rect {
fill-opacity: .5;
fill-opacity: 0.5;
stroke-width: 1;
}
}
@ -75,23 +76,24 @@
// padding: 5 5 0 5;
// }
.euiDataGrid--headerUnderline .euiDataGridHeaderCell {
border-bottom: 1px solid #D3DAE6 !important;
border-bottom: 1px solid #d3dae6 !important;
border-top: none !important;
}
.dscSidebar{
.euiButton--text, .euiButton--text:hover{
.dscSidebar {
.euiButton--text,
.euiButton--text:hover {
background-color: #fff;
}
}
.sidebar-list{
.sidebar-list {
max-width: 200px;
}
.euiSuperUpdateButton.euiButton--success {
background-color: #017D73 !important;
border-color: #017D73 !important;
color: #FFF !important;
background-color: #017d73 !important;
border-color: #017d73 !important;
color: #fff !important;
border-radius: 0 !important;
}
@ -109,6 +111,64 @@
background-color: transparent;
}
.kbnQueryBar__wrap{
.kbnQueryBar__wrap {
box-shadow: none;
}
}
.dscSetting {
position: absolute;
right: 0px;
top: 0;
&.setting-icon {
cursor: pointer;
padding: 0 4px;
&:hover {
background-color: #d3dae6;
}
}
&.setting-content {
background-color: #fff;
box-shadow: 0 2px 8px rgb(0 0 0 / 15%);
width: 300px;
max-height: 200px;
overflow-y: scroll;
z-index: 2;
.setting-wrapper {
padding: 10px;
}
.setting-row {
display: flex;
align-items: center;
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
.label {
// font-size: 12px;
width: 90px;
flex: none;
text-align: right;
margin-right: 5px;
}
}
}
}
.fake-chart {
position: relative;
.fake-mask {
position: absolute;
left: 0;
top: 0;
bottom: 0;
z-index: 1;
right: 0;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
font-size: 24px;
font-weight: 600;
background-color: rgba($color: #000000, $alpha: 0.15);
}
}

View File

@ -1,25 +1,40 @@
import Console from '../../components/kibana/console/components/Console';
import {connect} from 'dva';
import {Button, Icon, Menu, Dropdown, } from 'antd';
import Tabs from '@/components/infini/tabs';
import {DraggableTabs} from '@/components/infini/tabs/DraggableTabs';
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 {TabTitle} from './console_tab_title';
import '@/assets/utility.scss';
import Console from "../../components/kibana/console/components/Console";
import { connect } from "dva";
import { Button, Icon, Menu, Dropdown } from "antd";
import Tabs from "@/components/infini/tabs";
import { DraggableTabs } from "@/components/infini/tabs/DraggableTabs";
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 { TabTitle } from "./console_tab_title";
import "@/assets/utility.scss";
import { Resizable } from "re-resizable";
import {ResizeBar} from '@/components/infini/resize_bar';
import NewTabMenu from './NewTabMenu';
import { ResizeBar } from "@/components/infini/resize_bar";
import NewTabMenu from "./NewTabMenu";
import maximizeSvg from "@/assets/window-maximize.svg";
import restoreSvg from "@/assets/window-restore.svg";
const MaximizeIcon = (props = {}) => {
return <img height="14px" width="14px" {...props} src={maximizeSvg} />;
};
const RestoreIcon = (props = {}) => {
return <img height="14px" width="14px" {...props} src={restoreSvg} />;
};
const { TabPane } = Tabs;
const TabConsole = (props:any)=>{
return (
<Console {...props}/>
)
}
const TabConsole = (props: any) => {
return <Console {...props} />;
};
// export default connect(({
// global
@ -29,331 +44,381 @@ const TabConsole = (props:any)=>{
const addTab = (state: any, action: any) => {
const { panes } = state;
const {cluster} = action.payload;
const { cluster } = action.payload;
const activeKey = `${cluster.id}:${new Date().valueOf()}`;
panes.push({ key: activeKey, cluster_id: cluster.id, title: cluster.name});
panes.push({ key: activeKey, cluster_id: cluster.id, title: cluster.name });
return {
...state,
panes,
activeKey,
}
}
const removeTab = (state: any, action: any) =>{
};
};
const removeTab = (state: any, action: any) => {
const { activeKey, panes } = state;
const {targetKey} = action.payload;
const newPanes = panes.filter(pane => pane.key !== targetKey);
const { targetKey } = action.payload;
const newPanes = panes.filter((pane) => pane.key !== targetKey);
return {
...state,
panes: newPanes,
activeKey: panes[0]?.key,
}
}
activeKey: activeKey == targetKey ? newPanes[0]?.key : activeKey,
};
};
const consoleTabReducer = (state: any, action: any) => {
const {type, payload} = action;
const { type, payload } = action;
let newState = state;
switch(type){
case 'add':
switch (type) {
case "add":
newState = addTab(state, action);
break;
case 'remove':
newState = removeTab(state, action);
case "remove":
newState = removeTab(state, action);
break;
case 'change':
case "change":
newState = {
...state,
activeKey: payload.activeKey,
}
};
break;
case 'saveTitle':
const {key, title} = action.payload;
const newPanes = state.panes.map((pane: any)=>{
if(pane.key == key){
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)=>{
if(pane.key == state.activeKey){
case "saveContent":
const panes = state.panes.map((pane) => {
if (pane.key == state.activeKey) {
return {
...pane,
content: action.payload.content,
}
};
}
return pane;
});
newState = ({
newState = {
...state,
panes,
});
};
break;
case 'saveOrder':
newState = ({
case "saveOrder":
newState = {
...state,
order: action.payload.order,
});
};
default:
}
// setLocalState(newState);
return newState;
}
};
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{
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,
export const ConsoleUI = ({
selectedCluster,
clusterList,
clusterStatus,
minimize = false,
onMinimizeClick,
resizeable=false,
height='50vh'
}: any)=>{
const clusterMap = useMemo(()=>{
resizeable = false,
height = "50vh",
}: any) => {
const clusterMap = useMemo(() => {
let cm = {};
if(!clusterStatus){
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';
}
if (!clusterStatus[cluster.id]?.available) {
cluster.status = "unavailable";
}
cm[cluster.id] = cluster;
});
return cm;
}, [clusterList, clusterStatus])
const initialDefaultState = ()=>{
}, [clusterList, clusterStatus]);
const initialDefaultState = () => {
let defaultCluster = selectedCluster;
if(!defaultCluster.id){
defaultCluster = clusterList[0] ;
if (!defaultCluster.id) {
defaultCluster = clusterList[0];
}
const defaultActiveKey = `${defaultCluster.id || ''}:${new Date().valueOf()}`;
const defaultState = defaultCluster? {
panes:[{
key: defaultActiveKey, cluster_id: defaultCluster.id, title: defaultCluster.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])
const saveEditorContent = useCallback((content)=>{
dispatch({
type: 'saveContent',
payload: {
content
}
})
}, [dispatch])
const onChange = (activeKey:string) => {
dispatch({
type: 'change',
payload: {
activeKey
}
})
const defaultActiveKey = `${defaultCluster.id ||
""}:${new Date().valueOf()}`;
const defaultState = defaultCluster
? {
panes: [
{
key: defaultActiveKey,
cluster_id: defaultCluster.id,
title: defaultCluster.name,
},
],
activeKey: defaultActiveKey,
}
: { panes: [], activeKey: "" };
return defaultState;
};
const onEdit = (targetKey: string | React.MouseEvent<HTMLElement, MouseEvent>, action:string) => {
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]);
const saveEditorContent = useCallback(
(content) => {
dispatch({
type: "saveContent",
payload: {
content,
},
});
},
[dispatch]
);
const onChange = (activeKey: string) => {
dispatch({
type: "change",
payload: {
activeKey,
},
});
};
const onEdit = (
targetKey: string | React.MouseEvent<HTMLElement, MouseEvent>,
action: string
) => {
dispatch({
type: action,
payload: {
targetKey,
}
})
},
});
};
const newTabClick = useCallback((param: any)=>{
const cluster = clusterList.find(item=>item.id == param.id);
if(!cluster){
console.log('cluster not found')
return;
}
dispatch({
type:'add',
payload: {
cluster,
const newTabClick = useCallback(
(param: any) => {
const cluster = clusterList.find((item) => item.id == param.id);
if (!cluster) {
console.log("cluster not found");
return;
}
})
},[clusterList])
const menu = (
// <Menu onClick={newTabClick}>
// {(clusterList||[]).map((cluster:any)=>{
// return <Menu.Item key={cluster.id}>{cluster.name}</Menu.Item>
// })}
// </Menu>
<NewTabMenu data={clusterList}
onItemClick={newTabClick}
clusterStatus={clusterStatus}
size={10}
width="300px"/>
dispatch({
type: "add",
payload: {
cluster,
},
});
},
[clusterList]
);
// const menu = (
// // <Menu onClick={newTabClick}>
// // {(clusterList||[]).map((cluster:any)=>{
// // return <Menu.Item key={cluster.id}>{cluster.name}</Menu.Item>
// // })}
// // </Menu>
// <NewTabMenu data={clusterList}
// onItemClick={newTabClick}
// clusterStatus={clusterStatus}
// size={10}
// width="300px"/>
// );
const rootRef = useRef(null);
const [isFullscreen, setIsFullscreen] = useState(false);
const fullscreenClick = ()=>{
if(rootRef.current != null){
if(!isFullscreen){
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', '');
} else {
rootRef.current.className = rootRef.current.className.replace(
" fullscreen",
""
);
}
}
setEditorHeight(rootRef.current.clientHeight)
setIsFullscreen(!isFullscreen)
}
setEditorHeight(rootRef.current.clientHeight);
setIsFullscreen(!isFullscreen);
};
const tabBarExtra ={
left: <div>
{minimize? <Button size="small" onClick={onMinimizeClick} style={{marginLeft:5}} title="Close">
{/* <Icon type="minus"/> */}
<Icon type="poweroff"/>
</Button>:null}
</div>,
right:(
<div>
{isFullscreen?
<Button size="small" onClick={fullscreenClick}>
<Icon type="fullscreen-exit"/>
</Button>:
<Button size="small" onClick={fullscreenClick} title="Fullscreen">
<Icon type="fullscreen"/>
</Button>
}
const tabBarExtra = {
left: (
<div className="tabbar-icon" onClick={onMinimizeClick}>
{minimize ? <Icon type="code" /> : null}
</div>
),
append:(
<div>
<Dropdown overlay={menu} placement="bottomLeft">
<Button size="small" style={{marginRight:5}}>
<Icon type="plus"/>
</Button>
</Dropdown>
</div>
)
};
),
right: (
<>
<div className="tabbar-icon" onClick={onMinimizeClick}>
{minimize ? <Icon type="minus" /> : null}
</div>
<div className="tabbar-icon" onClick={fullscreenClick}>
{isFullscreen ? <RestoreIcon /> : <MaximizeIcon />}
</div>
</>
),
append: (
<NewTabMenu
data={clusterList}
onItemClick={newTabClick}
clusterStatus={clusterStatus}
size={10}
width="300px"
>
<div className="tabbar-icon">
<Icon type="plus" />
</div>
</NewTabMenu>
),
};
setClusterID(tabState.activeKey?.split(':')[0]);
const panes = tabState.panes.filter((pane: any)=>{
return typeof clusterMap[pane.cluster_id] != 'undefined';
})
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)=>{
const saveTitle = (key: string, title: string) => {
dispatch({
type:'saveTitle',
type: "saveTitle",
payload: {
key,
title,
}
})
}
const [editorHeight, setEditorHeight] = useState(calcHeightToPX(height))
const onResize = (_env, _dir, refToElement, delta)=>{
},
});
};
const [editorHeight, setEditorHeight] = useState(calcHeightToPX(height));
const onResize = (_env, _dir, refToElement, delta) => {
// console.log(refToElement.offsetHeight, delta)
setEditorHeight(refToElement.clientHeight)
}
setEditorHeight(refToElement.clientHeight);
};
const disableWindowScroll = ()=>{
document.body.style.overflow = 'hidden'
}
const disableWindowScroll = () => {
document.body.style.overflow = "hidden";
};
const enableWindowScroll = ()=>{
document.body.style.overflow = '';
}
const onTabNodeMoved=(newOrder:string[])=>{
const enableWindowScroll = () => {
document.body.style.overflow = "";
};
const onTabNodeMoved = (newOrder: string[]) => {
dispatch({
type:'saveOrder',
type: "saveOrder",
payload: {
order: newOrder,
}
})
}
order: newOrder,
},
});
};
return (
<Resizable
defaultSize={{
height: editorHeight||'50vh'
}}
minHeight={70}
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} >
<DraggableTabs
onChange={onChange}
activeKey={tabState.activeKey}
type="editable-card"
onEdit={onEdit}
hideAdd
initialOrder={tabState.order}
tabBarExtraContent={tabBarExtra}
onTabNodeMoved={onTabNodeMoved}
defaultSize={{
height: editorHeight || "50vh",
}}
minHeight={70}
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}
>
{panes.map(pane => (
<TabPane tab={<TabTitle title={pane.title} onTitleChange={(title)=>{saveTitle(pane.key, title)}}/>} key={pane.key} closable={pane.closable}>
<TabConsole height={editorHeight - 40} selectedCluster={clusterMap[pane.cluster_id]} paneKey={pane.key} saveEditorContent={saveEditorContent} initialText={pane.content} />
{/* {pane.content} */}
</TabPane>
))}
</DraggableTabs>
</div>
</Resizable>
<DraggableTabs
onChange={onChange}
activeKey={tabState.activeKey}
type="editable-card"
onEdit={onEdit}
hideAdd
initialOrder={tabState.order}
tabBarExtraContent={tabBarExtra}
onTabNodeMoved={onTabNodeMoved}
>
{panes.map((pane) => (
<TabPane
tab={
<TabTitle
title={pane.title}
onTitleChange={(title) => {
saveTitle(pane.key, title);
}}
/>
}
key={pane.key}
closable={pane.closable}
>
<TabConsole
height={editorHeight - 35}
selectedCluster={clusterMap[pane.cluster_id]}
paneKey={pane.key}
saveEditorContent={saveEditorContent}
initialText={pane.content}
/>
{/* {pane.content} */}
</TabPane>
))}
</DraggableTabs>
</div>
</Resizable>
);
}
};
export default connect(({
global
})=>({
export default connect(({ global }) => ({
selectedCluster: global.selectedCluster,
clusterList: global.clusterList,
clusterStatus: global.clusterStatus,
height: window.innerHeight - 75 + 'px',
}))(ConsoleUI);
height: window.innerHeight - 75 + "px",
}))(ConsoleUI);

View File

@ -22,11 +22,16 @@ class NewTabMenu extends React.Component{
initialLoad: true,
dataSource: [...props.data],
dataSourceKey: 1,
selectedIndex: -1,
overlayVisible: false,
}
}
componentDidMount(){
}
handleInfiniteOnLoad = (current) => {
let {size } = this.props;
let targetLength = current * size;
@ -54,17 +59,56 @@ class NewTabMenu extends React.Component{
dataSource: newData,
data: newData,
hasMore: newData.length > this.props.size,
selectedIndex: -1,
})
}
selectOffset = (offset)=> {
let {selectedIndex, data} = this.state;
const len = data.length;
selectedIndex = (selectedIndex + offset + len) % len;
// const item = data[selectedIndex];
this.setState({
selectedIndex,
})
}
onKeyDown = (e) => {
const { which } = e;
// e.preventDefault();
switch (which) {
case 38:
this.selectOffset(-1);
e.preventDefault();
e.stopPropagation()
break;
case 40:
this.selectOffset(1);
e.stopPropagation()
break;
case 13:
const {data, selectedIndex} = this.state;
if(selectedIndex > -1){
this.handleItemClick(data[selectedIndex]);
this.setState({ overlayVisible: false })
}
break;
}
}
render(){
const {clusterStatus} = this.props;
return (<div className={styles.dropmenu} style={{width: this.props.width}}>
<div className={styles.infiniteContainer} style={{height: this.props.height}}>
const menu = (<div className={styles.dropmenu} style={{width: this.props.width}}>
<div className={styles.infiniteContainer} style={{height: this.props.height}}
onMouseEnter={()=>{this.searchInputRef.focus()}}
tabIndex="0" onKeyDown={this.onKeyDown}>
<div className={styles.filter} style={{paddingTop: 10, paddingBottom:0}}>
<input className={styles['btn-ds']} style={{outline:'none'}} onChange={this.handleInputChange} placeholder="输入集群名称查找" value={this.state.displayValue||''} />
<input className={styles['btn-ds']} style={{outline:'none'}}
ref={(ref)=>{this.searchInputRef= ref;}}
onChange={this.handleInputChange} placeholder="输入集群名称查找" value={this.state.displayValue||''} />
</div>
<InfiniteScroll
initialLoad={this.state.initialLoad}
@ -74,11 +118,12 @@ class NewTabMenu extends React.Component{
>
<div className={styles.dslist}>
{(!this.state.data || !this.state.data.length)&& <div style={{display:'flex', justifyContent:'center', alignItems: 'center', height:50}}>匹配不到集群(匹配规则为前缀匹配)</div>}
{(this.state.data || []).map((item)=>{
{(this.state.data || []).map((item, idx)=>{
const cstatus = clusterStatus ? clusterStatus[item.id] : null;
return <DropdownItem key={item.id}
clusterItem={item}
clusterStatus={cstatus}
isSelected={this.state.selectedIndex == idx}
onClick={() => {
this.handleItemClick(item)
}}
@ -93,6 +138,17 @@ class NewTabMenu extends React.Component{
</div>
)}
</div>);
return (
<div>
<Dropdown overlay={menu} placement="bottomLeft"
visible={this.state.overlayVisible}
onVisibleChange={(flag)=>{
this.setState({ overlayVisible: flag });
}}>
{this.props.children}
</Dropdown>
</div>
)
}
}

View File

@ -1,15 +1,28 @@
.tab-title{
display: inline-block;
height: 30px;
overflow: hidden;
.input-eidtor{
border-radius: 0;
border:none;
border-bottom: 1px solid #ccc;
// border-bottom: 1px solid #ccc;
border-right: none;
padding-left: 1em;
color:rgba(0, 0, 0, 0.65);
height: 28px;
line-height: 28px;
background-color: #939ea0;
color: #fff;
&:focus{
outline: none;
}
}
.icon-cont{
overflow: hidden;
height: 100%;
display: flex;
align-items: center;
}
}
#console{

View File

@ -26,16 +26,26 @@ export const TabTitle = ({title, onTitleChange}: TabTitleProps)=>{
}
},[editable])
const inputRef = useRef(null);
const onKeyDown = (e: any)=>{
const { which } = e;
switch (which) {
case 13:
e.target.blur();
break;
}
}
return (<div title="double click to change title" className="tab-title" onDoubleClick={()=>{
setEditable(true)
}}>
{editable ? <input ref={inputRef} className="input-eidtor"
{editable ? <input ref={inputRef} className="input-eidtor" onKeyDown={onKeyDown}
type="text" value={value}
onBlur={()=>{
setEditable(false)
}}
onChange={onValueChange}/>:
<div style={{display:'flex', alignItems:'center'}}><Icon component={ElasticIcon} />{value}</div>}
<div className="icon-cont"><Icon component={ElasticIcon} />{value}</div>}
</div>)
}