chore: adjust TopN (#96)
* chore: optimize the chart when only the color metric is selected in TopN * chore: add metric_config to form of View's field * feat: add `Complex Field` to `View` * chore: adjust topn's metrics * fix: fix the issue of switching metrics in TopN * chore: add columns(function, format) to `Complex fields` * chore: verify if the complex field is builtin * fix: set func to max by default in create complex field * chore: change text of button for fetching data to `Apply` in TopN * chore: check if metric support some statistics if rollup is enabled * fix: add `time_field`` to params of fetching data if statistic is `latency`` * chore: check if field support some statistics if rollup is enabled * chore: remove layout in view * fix: format does not work in TopN * fix: adjust filters of fetching data in TopN --------- Co-authored-by: yaojiping <yaojiping@infini.ltd>
This commit is contained in:
parent
16c08e745f
commit
d828fe8781
|
@ -531,6 +531,7 @@ const Index = forwardRef((props, ref) => {
|
||||||
<WidgetRender
|
<WidgetRender
|
||||||
widget={histogramState.widget}
|
widget={histogramState.widget}
|
||||||
range={histogramState.range}
|
range={histogramState.range}
|
||||||
|
queryParams={queryParams?.filters || {}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { getTimezone } from "@/utils/utils";
|
||||||
import { getContext } from "@/pages/DataManagement/context";
|
import { getContext } from "@/pages/DataManagement/context";
|
||||||
import { ESPrefix } from "@/services/common";
|
import { ESPrefix } from "@/services/common";
|
||||||
import CollectStatus from "@/components/CollectStatus";
|
import CollectStatus from "@/components/CollectStatus";
|
||||||
|
import styles from "./index.less"
|
||||||
|
|
||||||
const { TabPane } = Tabs;
|
const { TabPane } = Tabs;
|
||||||
|
|
||||||
|
@ -140,7 +141,7 @@ const Monitor = (props) => {
|
||||||
<div>
|
<div>
|
||||||
<BreadcrumbList data={breadcrumbList} />
|
<BreadcrumbList data={breadcrumbList} />
|
||||||
|
|
||||||
<Card bodyStyle={{ padding: 15 }}>
|
<Card bodyStyle={{ padding: 16 }}>
|
||||||
{
|
{
|
||||||
selectedCluster?.id ? (
|
selectedCluster?.id ? (
|
||||||
<>
|
<>
|
||||||
|
@ -187,58 +188,59 @@ const Monitor = (props) => {
|
||||||
<CollectStatus fetchUrl={`${ESPrefix}/${selectedCluster?.id}/_collection_stats`}/>
|
<CollectStatus fetchUrl={`${ESPrefix}/${selectedCluster?.id}/_collection_stats`}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className={styles.tabs}>
|
||||||
<Tabs
|
<Tabs
|
||||||
activeKey={param?.tab || panes[0]?.key}
|
activeKey={param?.tab || panes[0]?.key}
|
||||||
onChange={(key) => {
|
onChange={(key) => {
|
||||||
setParam({ ...param, tab: key });
|
setParam({ ...param, tab: key });
|
||||||
}}
|
}}
|
||||||
tabBarGutter={10}
|
tabBarGutter={10}
|
||||||
destroyInactiveTabPane
|
destroyInactiveTabPane
|
||||||
animated={false}
|
animated={false}
|
||||||
>
|
>
|
||||||
{panes.map((pane) => (
|
{panes.map((pane) => (
|
||||||
<TabPane tab={pane.title} key={pane.key}>
|
<TabPane tab={pane.title} key={pane.key}>
|
||||||
<Spin spinning={spinning && !!state.refresh}>
|
<Spin spinning={spinning && !!state.refresh}>
|
||||||
<StatisticBar
|
<StatisticBar
|
||||||
setSpinning={setSpinning}
|
setSpinning={setSpinning}
|
||||||
onInfoChange={onInfoChange}
|
onInfoChange={onInfoChange}
|
||||||
{...state}
|
{...state}
|
||||||
{...extraParams}
|
{...extraParams}
|
||||||
/>
|
/>
|
||||||
</Spin>
|
</Spin>
|
||||||
<div style={{ marginTop: 15 }}>
|
<div style={{ marginTop: 15 }}>
|
||||||
{checkPaneParams({
|
{checkPaneParams({
|
||||||
...state,
|
...state,
|
||||||
...extraParams,
|
...extraParams,
|
||||||
}) ? (
|
}) ? (
|
||||||
typeof pane.component == "string" ? (
|
typeof pane.component == "string" ? (
|
||||||
pane.component
|
pane.component
|
||||||
) : (
|
) : (
|
||||||
<pane.component
|
<pane.component
|
||||||
selectedCluster={selectedCluster}
|
selectedCluster={selectedCluster}
|
||||||
isAgent={isAgent}
|
isAgent={isAgent}
|
||||||
{...state}
|
{...state}
|
||||||
handleTimeChange={handleTimeChange}
|
handleTimeChange={handleTimeChange}
|
||||||
handleTimeIntervalChange={(timeInterval) => {
|
handleTimeIntervalChange={(timeInterval) => {
|
||||||
onTimeSettingsChange({
|
onTimeSettingsChange({
|
||||||
timeInterval,
|
timeInterval,
|
||||||
})
|
})
|
||||||
setState({
|
setState({
|
||||||
...state,
|
...state,
|
||||||
timeInterval,
|
timeInterval,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
setSpinning={setSpinning}
|
setSpinning={setSpinning}
|
||||||
{...extraParams}
|
{...extraParams}
|
||||||
bucketSize={state.timeInterval}
|
bucketSize={state.timeInterval}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
) : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
) : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
.tabs {
|
||||||
|
:global {
|
||||||
|
.ant-tabs .ant-tabs-right-content {
|
||||||
|
padding-right: 16px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -43,7 +43,7 @@ export class SimpleSavedObject<T = unknown> {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private client: SavedObjectsClientContract,
|
private client: SavedObjectsClientContract,
|
||||||
{ id, type, version, attributes, error, references, migrationVersion }: SavedObjectType<T>
|
{ id, type, version, attributes, error, references, migrationVersion, complex_fields }: SavedObjectType<T>
|
||||||
) {
|
) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
|
@ -51,6 +51,7 @@ export class SimpleSavedObject<T = unknown> {
|
||||||
this.references = references || [];
|
this.references = references || [];
|
||||||
this._version = version;
|
this._version = version;
|
||||||
this.migrationVersion = migrationVersion;
|
this.migrationVersion = migrationVersion;
|
||||||
|
this.attributes.complexFields = complex_fields
|
||||||
if (error) {
|
if (error) {
|
||||||
this.error = error;
|
this.error = error;
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@ export const fieldList = (
|
||||||
}
|
}
|
||||||
this.groups.get(field.type)!.set(field.name, field);
|
this.groups.get(field.type)!.set(field.name, field);
|
||||||
};
|
};
|
||||||
private removeByGroup = (field: IFieldType) => this.groups.get(field.type)!.delete(field.name);
|
private removeByGroup = (field: IFieldType) => this.groups.get(field.type)?.delete(field.name);
|
||||||
private calcDisplayName = (name: string) =>
|
private calcDisplayName = (name: string) =>
|
||||||
shortDotsEnable ? shortenDottedString(name) : name;
|
shortDotsEnable ? shortenDottedString(name) : name;
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -71,7 +71,7 @@ export const fieldList = (
|
||||||
...(this.groups.get(type) || new Map()).values(),
|
...(this.groups.get(type) || new Map()).values(),
|
||||||
];
|
];
|
||||||
public readonly add = (field: FieldSpec) => {
|
public readonly add = (field: FieldSpec) => {
|
||||||
const newField = new IndexPatternField(field, this.calcDisplayName(field.name));
|
const newField = new IndexPatternField(field, this.calcDisplayName(field.displayName || field.name));
|
||||||
this.push(newField);
|
this.push(newField);
|
||||||
this.setByName(newField);
|
this.setByName(newField);
|
||||||
this.setByGroup(newField);
|
this.setByGroup(newField);
|
||||||
|
|
|
@ -134,6 +134,14 @@ export class IndexPatternField implements IFieldType {
|
||||||
return this.aggregatable && !notVisualizableFieldTypes.includes(this.spec.type);
|
return this.aggregatable && !notVisualizableFieldTypes.includes(this.spec.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public set metric_config(metric_config) {
|
||||||
|
this.spec.metric_config = metric_config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get metric_config() {
|
||||||
|
return this.spec.metric_config;
|
||||||
|
}
|
||||||
|
|
||||||
public toJSON() {
|
public toJSON() {
|
||||||
return {
|
return {
|
||||||
count: this.count,
|
count: this.count,
|
||||||
|
@ -148,7 +156,8 @@ export class IndexPatternField implements IFieldType {
|
||||||
searchable: this.searchable,
|
searchable: this.searchable,
|
||||||
aggregatable: this.aggregatable,
|
aggregatable: this.aggregatable,
|
||||||
readFromDocValues: this.readFromDocValues,
|
readFromDocValues: this.readFromDocValues,
|
||||||
subType: this.subType,
|
subType: this.subType,
|
||||||
|
metric_config: this.metric_config,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,6 +180,7 @@ export class IndexPatternField implements IFieldType {
|
||||||
readFromDocValues: this.readFromDocValues,
|
readFromDocValues: this.readFromDocValues,
|
||||||
subType: this.subType,
|
subType: this.subType,
|
||||||
format: getFormatterForField ? getFormatterForField(this).toJSON() : undefined,
|
format: getFormatterForField ? getFormatterForField(this).toJSON() : undefined,
|
||||||
|
metric_config: this.metric_config,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,7 +117,14 @@ export class IndexPattern implements IIndexPattern {
|
||||||
|
|
||||||
// set values
|
// set values
|
||||||
this.id = spec.id;
|
this.id = spec.id;
|
||||||
const fieldFormatMap = this.fieldSpecsToFieldFormatMap(spec.fields);
|
|
||||||
|
this.complexFields = fieldList([], this.shortDotsEnable);
|
||||||
|
this.complexFields.replaceAll(this.complexFieldsToArray(spec.complexFields));
|
||||||
|
|
||||||
|
const fieldFormatMap = {
|
||||||
|
...this.fieldSpecsToFieldFormatMap(spec.fields),
|
||||||
|
...this.complexfieldSpecsToFieldFormatMap(spec.complexFields)
|
||||||
|
}
|
||||||
|
|
||||||
this.version = spec.version;
|
this.version = spec.version;
|
||||||
|
|
||||||
|
@ -185,6 +192,31 @@ export class IndexPattern implements IIndexPattern {
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private complexfieldSpecsToFieldFormatMap = (
|
||||||
|
fldList: IndexPatternSpec["fields"] = {}
|
||||||
|
) =>
|
||||||
|
Object.entries(fldList).reduce<Record<string, SerializedFieldFormat>>(
|
||||||
|
(col, [key, fieldSpec]) => {
|
||||||
|
if (fieldSpec.format) {
|
||||||
|
col[key] = { ...fieldSpec.format };
|
||||||
|
}
|
||||||
|
return col;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
private complexFieldsToArray = (complexFields) => {
|
||||||
|
const keys = Object.keys(complexFields || {})
|
||||||
|
return keys.map((key) => {
|
||||||
|
const item = complexFields?.[key] || {}
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
name: key,
|
||||||
|
metric_name: item.name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
getComputedFields() {
|
getComputedFields() {
|
||||||
const scriptFields: any = {};
|
const scriptFields: any = {};
|
||||||
if (!this.fields) {
|
if (!this.fields) {
|
||||||
|
@ -381,6 +413,20 @@ export class IndexPattern implements IIndexPattern {
|
||||||
? undefined
|
? undefined
|
||||||
: JSON.stringify(serialized);
|
: JSON.stringify(serialized);
|
||||||
|
|
||||||
|
let formatComplexFields
|
||||||
|
if (this.complexFields) {
|
||||||
|
formatComplexFields = {}
|
||||||
|
this.complexFields.map((item) => {
|
||||||
|
if (item.spec?.name) {
|
||||||
|
const { metric_name, format, type, ...rest } = item.spec
|
||||||
|
formatComplexFields[item.spec.name] = {
|
||||||
|
...rest,
|
||||||
|
name: metric_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: this.title,
|
title: this.title,
|
||||||
viewName: this.viewName,
|
viewName: this.viewName,
|
||||||
|
@ -390,6 +436,7 @@ export class IndexPattern implements IIndexPattern {
|
||||||
? JSON.stringify(this.sourceFilters)
|
? JSON.stringify(this.sourceFilters)
|
||||||
: undefined,
|
: undefined,
|
||||||
fields: this.fields ? JSON.stringify(this.fields) : undefined,
|
fields: this.fields ? JSON.stringify(this.fields) : undefined,
|
||||||
|
complex_fields: formatComplexFields ? JSON.stringify(formatComplexFields) : undefined,
|
||||||
fieldFormatMap,
|
fieldFormatMap,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
typeMeta: this.typeMeta ? JSON.stringify(this.typeMeta) : undefined,
|
typeMeta: this.typeMeta ? JSON.stringify(this.typeMeta) : undefined,
|
||||||
|
|
|
@ -358,6 +358,7 @@ export class IndexPatternsService {
|
||||||
sourceFilters,
|
sourceFilters,
|
||||||
fieldFormatMap,
|
fieldFormatMap,
|
||||||
typeMeta,
|
typeMeta,
|
||||||
|
complexFields,
|
||||||
},
|
},
|
||||||
type,
|
type,
|
||||||
} = savedObject;
|
} = savedObject;
|
||||||
|
@ -370,6 +371,7 @@ export class IndexPatternsService {
|
||||||
? JSON.parse(fieldFormatMap)
|
? JSON.parse(fieldFormatMap)
|
||||||
: {};
|
: {};
|
||||||
const parsedFields: FieldSpec[] = fields ? JSON.parse(fields) : [];
|
const parsedFields: FieldSpec[] = fields ? JSON.parse(fields) : [];
|
||||||
|
const parsedComplexFields = complexFields ? JSON.parse(complexFields) : [];
|
||||||
|
|
||||||
this.addFormatsToFields(parsedFields, parsedFieldFormatMap);
|
this.addFormatsToFields(parsedFields, parsedFieldFormatMap);
|
||||||
return {
|
return {
|
||||||
|
@ -383,6 +385,7 @@ export class IndexPatternsService {
|
||||||
fields: this.fieldArrayToMap(parsedFields),
|
fields: this.fieldArrayToMap(parsedFields),
|
||||||
typeMeta: parsedTypeMeta,
|
typeMeta: parsedTypeMeta,
|
||||||
type,
|
type,
|
||||||
|
complexFields: parsedComplexFields
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -409,7 +412,6 @@ export class IndexPatternsService {
|
||||||
// if (!savedObject.version) {
|
// if (!savedObject.version) {
|
||||||
// throw new SavedObjectNotFound(savedObjectType, id, 'management/kibana/indexPatterns');
|
// throw new SavedObjectNotFound(savedObjectType, id, 'management/kibana/indexPatterns');
|
||||||
// }
|
// }
|
||||||
|
|
||||||
const spec = this.savedObjectToSpec(savedObject);
|
const spec = this.savedObjectToSpec(savedObject);
|
||||||
const { title, type, typeMeta } = spec;
|
const { title, type, typeMeta } = spec;
|
||||||
const parsedFieldFormats: FieldFormatMap = savedObject.attributes
|
const parsedFieldFormats: FieldFormatMap = savedObject.attributes
|
||||||
|
@ -512,7 +514,6 @@ export class IndexPatternsService {
|
||||||
UI_SETTINGS.SHORT_DOTS_ENABLE
|
UI_SETTINGS.SHORT_DOTS_ENABLE
|
||||||
);
|
);
|
||||||
const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS);
|
const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS);
|
||||||
|
|
||||||
const indexPattern = new IndexPattern({
|
const indexPattern = new IndexPattern({
|
||||||
spec,
|
spec,
|
||||||
savedObjectsClient: this.savedObjectsClient,
|
savedObjectsClient: this.savedObjectsClient,
|
||||||
|
@ -623,8 +624,12 @@ export class IndexPatternsService {
|
||||||
version: indexPattern.version,
|
version: indexPattern.version,
|
||||||
})
|
})
|
||||||
.then((resp) => {
|
.then((resp) => {
|
||||||
indexPattern.id = resp.id;
|
if (resp.id) {
|
||||||
indexPattern.version = resp.version;
|
indexPattern.id = resp.id;
|
||||||
|
}
|
||||||
|
if (resp.version) {
|
||||||
|
indexPattern.version = resp.version;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(async (err) => {
|
.catch(async (err) => {
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -18,5 +18,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const TAB_INDEXED_FIELDS = 'indexedFields';
|
export const TAB_INDEXED_FIELDS = 'indexedFields';
|
||||||
|
export const TAB_COMPLEX_FIELDS = 'complexFields';
|
||||||
export const TAB_SCRIPTED_FIELDS = 'scriptedFields';
|
export const TAB_SCRIPTED_FIELDS = 'scriptedFields';
|
||||||
export const TAB_SOURCE_FILTERS = 'sourceFilters';
|
export const TAB_SOURCE_FILTERS = 'sourceFilters';
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||||
|
* license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright
|
||||||
|
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||||
|
* the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
* not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
import React from "react";
|
||||||
|
import { withRouter, RouteComponentProps } from "react-router-dom";
|
||||||
|
|
||||||
|
import { EuiFlexGroup, EuiFlexItem, EuiPanel } from "@elastic/eui";
|
||||||
|
// import { IndexPattern, IndexPatternField } from '../../../../../../plugins/data/public';
|
||||||
|
// import { useKibana } from '../../../../../../plugins/kibana_react/public';
|
||||||
|
// import { IndexPatternManagmentContext } from '../../../types';
|
||||||
|
import { IndexHeader } from "../index_header";
|
||||||
|
import { TAB_SCRIPTED_FIELDS, TAB_INDEXED_FIELDS, TAB_COMPLEX_FIELDS } from "../constants";
|
||||||
|
|
||||||
|
import { ComplexFieldEditor } from "../../field_editor/complex_field_editor";
|
||||||
|
import { useGlobalContext } from "../../../context";
|
||||||
|
import { IndexPattern, IndexPatternField } from "../../../import";
|
||||||
|
|
||||||
|
interface CreateEditFieldProps extends RouteComponentProps {
|
||||||
|
indexPattern: IndexPattern;
|
||||||
|
mode?: string;
|
||||||
|
fieldName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreateEditComplexField = withRouter(
|
||||||
|
({ indexPattern, mode, fieldName, history }: CreateEditFieldProps) => {
|
||||||
|
const { uiSettings, data } = useGlobalContext();
|
||||||
|
const spec =
|
||||||
|
mode === "edit" && fieldName
|
||||||
|
? indexPattern.complexFields.getByName(fieldName)?.spec
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const url = `/patterns/${indexPattern.id}?_a=(tab:complexFields)`;
|
||||||
|
|
||||||
|
if (mode === "edit" && !spec) {
|
||||||
|
history.push(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectAway = () => {
|
||||||
|
history.push(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiPanel paddingSize={"l"}>
|
||||||
|
<EuiFlexGroup direction="column">
|
||||||
|
<EuiFlexItem>
|
||||||
|
<IndexHeader
|
||||||
|
indexPattern={indexPattern}
|
||||||
|
defaultIndex={uiSettings.get("defaultIndex")}
|
||||||
|
/>
|
||||||
|
</EuiFlexItem>
|
||||||
|
<EuiFlexItem>
|
||||||
|
<ComplexFieldEditor
|
||||||
|
indexPattern={indexPattern}
|
||||||
|
spec={spec || {}}
|
||||||
|
services={{
|
||||||
|
saveIndexPattern: data.indexPatterns.updateSavedObject.bind(
|
||||||
|
data.indexPatterns
|
||||||
|
),
|
||||||
|
redirectAway,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
</EuiPanel>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||||
|
* license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright
|
||||||
|
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||||
|
* the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
* not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
|
// import { IndexPattern } from '../../../../../../plugins/data/public';
|
||||||
|
// import { getEditFieldBreadcrumbs, getCreateFieldBreadcrumbs } from '../../breadcrumbs';
|
||||||
|
// import { useKibana } from '../../../../../../plugins/kibana_react/public';
|
||||||
|
// import { IndexPatternManagmentContext } from '../../../types';
|
||||||
|
import { CreateEditComplexField } from './create_edit_complex_field';
|
||||||
|
import { IndexPattern } from '../../../import';
|
||||||
|
import { useGlobalContext } from '../../../context';
|
||||||
|
|
||||||
|
export type CreateEditFieldContainerProps = RouteComponentProps<{ id: string; fieldName: string }>;
|
||||||
|
|
||||||
|
const CreateEditFieldCont: React.FC<CreateEditFieldContainerProps> = ({ ...props }) => {
|
||||||
|
// const { setBreadcrumbs, data } = useKibana<IndexPatternManagmentContext>().services;
|
||||||
|
const {data} = useGlobalContext()
|
||||||
|
const [indexPattern, setIndexPattern] = useState<IndexPattern>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
data.indexPatterns.get(props.match.params.id).then((ip: IndexPattern) => {
|
||||||
|
setIndexPattern(ip);
|
||||||
|
// if (ip) {
|
||||||
|
// setBreadcrumbs(
|
||||||
|
// props.match.params.fieldName
|
||||||
|
// ? getEditFieldBreadcrumbs(ip, props.match.params.fieldName)
|
||||||
|
// : getCreateFieldBreadcrumbs(ip)
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
}, [props.match.params.id, props.match.params.fieldName, data.indexPatterns]);//setBreadcrumbs
|
||||||
|
|
||||||
|
if (indexPattern) {
|
||||||
|
return (
|
||||||
|
<CreateEditComplexField
|
||||||
|
indexPattern={indexPattern}
|
||||||
|
mode={props.match.params.fieldName ? 'edit' : 'create'}
|
||||||
|
fieldName={props.match.params.fieldName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CreateEditComplexFieldContainer = withRouter(CreateEditFieldCont);
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||||
|
* license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright
|
||||||
|
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||||
|
* the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
* not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { CreateEditComplexField } from './create_edit_complex_field';
|
||||||
|
export { CreateEditComplexFieldContainer } from './create_edit_complex_field_container';
|
|
@ -0,0 +1,164 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import { createSelector } from "reselect";
|
||||||
|
import { Table } from "./components/table";
|
||||||
|
import { getFieldFormat } from "./lib";
|
||||||
|
import { IndexPatternField, IndexPattern, IFieldType } from "../../../import";
|
||||||
|
import { EuiContext } from "@elastic/eui";
|
||||||
|
import { formatMessage } from "umi/locale";
|
||||||
|
import { router } from "umi";
|
||||||
|
|
||||||
|
export class ComplexFieldsTable extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
fields: this.mapFields(this.props.indexPattern?.complexFields),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||||
|
if (nextProps.indexPattern?.complexFields !== this.props.indexPattern?.complexFields) {
|
||||||
|
this.setState({
|
||||||
|
fields: this.mapFields(nextProps.indexPattern?.complexFields),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mapFields(fields) {
|
||||||
|
const { indexPattern, fieldWildcardMatcher, helpers } = this.props;
|
||||||
|
const sourceFilters =
|
||||||
|
indexPattern.sourceFilters &&
|
||||||
|
indexPattern.sourceFilters.map((f) => f.value);
|
||||||
|
const fieldWildcardMatch = fieldWildcardMatcher ? fieldWildcardMatcher(sourceFilters || []) : undefined;
|
||||||
|
return (
|
||||||
|
(fields &&
|
||||||
|
fields.map((field) => {
|
||||||
|
const func = Object.keys(field.spec?.function || {})[0]
|
||||||
|
return {
|
||||||
|
...field.spec,
|
||||||
|
displayName: field.spec.metric_name,
|
||||||
|
func: func,
|
||||||
|
format: getFieldFormat(indexPattern, field.name),
|
||||||
|
excluded: fieldWildcardMatch
|
||||||
|
? fieldWildcardMatch(field.name)
|
||||||
|
: false,
|
||||||
|
info:
|
||||||
|
helpers.getFieldInfo && helpers.getFieldInfo(indexPattern, field),
|
||||||
|
};
|
||||||
|
})) ||
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFilteredFields = createSelector(
|
||||||
|
(state) => state.fields,
|
||||||
|
(state, props) =>
|
||||||
|
props.fieldFilter,
|
||||||
|
(state, props) =>
|
||||||
|
props.indexedFieldTypeFilter,
|
||||||
|
(fields, fieldFilter, indexedFieldTypeFilter) => {
|
||||||
|
if (fieldFilter) {
|
||||||
|
const normalizedFieldFilter = fieldFilter.toLowerCase();
|
||||||
|
fields = fields.filter((field) =>
|
||||||
|
field.name.toLowerCase().includes(normalizedFieldFilter)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indexedFieldTypeFilter) {
|
||||||
|
fields = fields.filter(
|
||||||
|
(field) => field.type === indexedFieldTypeFilter
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { indexPattern } = this.props;
|
||||||
|
const fields = this.getFilteredFields(this.state, {...this.props, fields: indexPattern?.complexFields});
|
||||||
|
const editField = (field) => this.props.helpers.redirectToRoute(field)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<EuiContext
|
||||||
|
i18n={{
|
||||||
|
mapping: {
|
||||||
|
"euiTablePagination.rowsPerPage": formatMessage({
|
||||||
|
id: "explore.table.rows_of_page",
|
||||||
|
}),
|
||||||
|
"euiTablePagination.rowsPerPageOption":
|
||||||
|
"{rowsPerPage} " +
|
||||||
|
formatMessage({ id: "explore.table.rows_of_page_option" }),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
indexPattern={indexPattern}
|
||||||
|
items={fields}
|
||||||
|
editField={editField}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
field: 'displayName',
|
||||||
|
name: 'Name',
|
||||||
|
dataType: 'string',
|
||||||
|
sortable: true,
|
||||||
|
render: (value) => value,
|
||||||
|
'data-test-subj': 'complexFieldName',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'func',
|
||||||
|
name: 'Function',
|
||||||
|
dataType: 'string',
|
||||||
|
sortable: true,
|
||||||
|
render: (value) => value ? value.toUpperCase() : '-',
|
||||||
|
'data-test-subj': 'complexFieldFunc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'format',
|
||||||
|
name: 'Format',
|
||||||
|
dataType: 'string',
|
||||||
|
sortable: true,
|
||||||
|
render: (value) => value || 'Number',
|
||||||
|
'data-test-subj': 'complexFieldFormat',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'tags',
|
||||||
|
name: 'Tags',
|
||||||
|
dataType: 'auto',
|
||||||
|
render: (value) => {
|
||||||
|
return value?.join(', ');
|
||||||
|
},
|
||||||
|
'data-test-subj': 'complexFieldTags',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "builtin",
|
||||||
|
name: 'Builtin',
|
||||||
|
dataType: 'auto',
|
||||||
|
render: (value) => {
|
||||||
|
return value === true ? "true" : "false";
|
||||||
|
},
|
||||||
|
'data-test-subj': 'complexFieldBuiltin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'Edit',
|
||||||
|
description: 'Edit',
|
||||||
|
icon: 'pencil',
|
||||||
|
onClick: editField,
|
||||||
|
type: 'icon',
|
||||||
|
'data-test-subj': 'editFieldFormat',
|
||||||
|
available: ({ builtin }) => !builtin,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
width: '40px',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</EuiContext>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -139,7 +139,7 @@ export class Table extends PureComponent<IndexedFieldProps> {
|
||||||
pageSizeOptions: [5, 10, 25, 50],
|
pageSizeOptions: [5, 10, 25, 50],
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns: Array<EuiBasicTableColumn<IndexedFieldItem>> = [
|
const columns: Array<EuiBasicTableColumn<IndexedFieldItem>> = this.props.columns || [
|
||||||
{
|
{
|
||||||
field: 'displayName',
|
field: 'displayName',
|
||||||
name: nameHeader,
|
name: nameHeader,
|
||||||
|
|
|
@ -34,6 +34,7 @@ import {
|
||||||
EuiFieldSearch,
|
EuiFieldSearch,
|
||||||
EuiSelect,
|
EuiSelect,
|
||||||
EuiSelectOption,
|
EuiSelectOption,
|
||||||
|
EuiButton,
|
||||||
} from "@elastic/eui";
|
} from "@elastic/eui";
|
||||||
// import { fieldWildcardMatcher } from '../../../../../utils/public';
|
// import { fieldWildcardMatcher } from '../../../../../utils/public';
|
||||||
import {
|
import {
|
||||||
|
@ -46,6 +47,7 @@ import {
|
||||||
// import { IndexPatternManagmentContext } from '../../../types';
|
// import { IndexPatternManagmentContext } from '../../../types';
|
||||||
import { createEditIndexPatternPageStateContainer } from "../edit_index_pattern_state_container";
|
import { createEditIndexPatternPageStateContainer } from "../edit_index_pattern_state_container";
|
||||||
import {
|
import {
|
||||||
|
TAB_COMPLEX_FIELDS,
|
||||||
TAB_INDEXED_FIELDS,
|
TAB_INDEXED_FIELDS,
|
||||||
TAB_SCRIPTED_FIELDS,
|
TAB_SCRIPTED_FIELDS,
|
||||||
TAB_SOURCE_FILTERS,
|
TAB_SOURCE_FILTERS,
|
||||||
|
@ -64,6 +66,7 @@ import {
|
||||||
import { useGlobalContext } from "../../../context";
|
import { useGlobalContext } from "../../../context";
|
||||||
|
|
||||||
import LayoutList from "@/pages/DataManagement/View/LayoutList"
|
import LayoutList from "@/pages/DataManagement/View/LayoutList"
|
||||||
|
import { ComplexFieldsTable } from "../indexed_fields_table/complex_fields_table";
|
||||||
|
|
||||||
interface TabsProps extends Pick<RouteComponentProps, "history" | "location"> {
|
interface TabsProps extends Pick<RouteComponentProps, "history" | "location"> {
|
||||||
indexPattern: IndexPattern;
|
indexPattern: IndexPattern;
|
||||||
|
@ -95,6 +98,7 @@ export function Tabs({
|
||||||
indexPatternFieldEditor,
|
indexPatternFieldEditor,
|
||||||
} = useGlobalContext();
|
} = useGlobalContext();
|
||||||
const [fieldFilter, setFieldFilter] = useState<string>("");
|
const [fieldFilter, setFieldFilter] = useState<string>("");
|
||||||
|
const [complexFieldFilter, setComplexFieldFilter] = useState<string>("");
|
||||||
const [indexedFieldTypeFilter, setIndexedFieldTypeFilter] = useState<string>(
|
const [indexedFieldTypeFilter, setIndexedFieldTypeFilter] = useState<string>(
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
@ -193,6 +197,39 @@ export function Tabs({
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getComplexFilterSection = useCallback(
|
||||||
|
() => {
|
||||||
|
return (
|
||||||
|
<EuiFlexGroup>
|
||||||
|
<EuiFlexItem grow={true}>
|
||||||
|
<EuiFieldSearch
|
||||||
|
fullWidth
|
||||||
|
placeholder={filterPlaceholder}
|
||||||
|
value={complexFieldFilter}
|
||||||
|
onChange={(e) => setComplexFieldFilter(e.target.value)}
|
||||||
|
data-test-subj="complexFieldFilter"
|
||||||
|
aria-label={searchAriaLabel}
|
||||||
|
/>
|
||||||
|
</EuiFlexItem>
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiButton
|
||||||
|
fill
|
||||||
|
onClick={() => {
|
||||||
|
history.push(`/patterns/${indexPattern?.id}/complex/create`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{"Create field"}
|
||||||
|
</EuiButton>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[
|
||||||
|
complexFieldFilter,
|
||||||
|
indexPattern,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
const getContent = useCallback(
|
const getContent = useCallback(
|
||||||
(type: string) => {
|
(type: string) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
@ -217,6 +254,25 @@ export function Tabs({
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
case TAB_COMPLEX_FIELDS:
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<EuiSpacer size="m" />
|
||||||
|
{getComplexFilterSection()}
|
||||||
|
<EuiSpacer size="m" />
|
||||||
|
<ComplexFieldsTable
|
||||||
|
fields={fields}
|
||||||
|
indexPattern={indexPattern}
|
||||||
|
fieldFilter={complexFieldFilter}
|
||||||
|
helpers={{
|
||||||
|
redirectToRoute: (field) => {
|
||||||
|
history.push(`/patterns/${indexPattern?.id}/complex/${field?.name}/edit`);
|
||||||
|
},
|
||||||
|
getFieldInfo: indexPatternManagementStart.list.getFieldInfo,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
case TAB_SCRIPTED_FIELDS:
|
case TAB_SCRIPTED_FIELDS:
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
@ -261,6 +317,7 @@ export function Tabs({
|
||||||
fieldWildcardMatcherDecorated,
|
fieldWildcardMatcherDecorated,
|
||||||
fields,
|
fields,
|
||||||
getFilterSection,
|
getFilterSection,
|
||||||
|
getComplexFilterSection,
|
||||||
history,
|
history,
|
||||||
indexPattern,
|
indexPattern,
|
||||||
indexPatternManagementStart.list.getFieldInfo,
|
indexPatternManagementStart.list.getFieldInfo,
|
||||||
|
@ -272,20 +329,30 @@ export function Tabs({
|
||||||
);
|
);
|
||||||
|
|
||||||
const euiTabs: EuiTabbedContentTab[] = useMemo(
|
const euiTabs: EuiTabbedContentTab[] = useMemo(
|
||||||
() =>
|
() => {
|
||||||
getTabs(indexPattern, fieldFilter, indexPatternManagementStart.list).map(
|
const tabs = getTabs(indexPattern, fieldFilter, indexPatternManagementStart.list).map(
|
||||||
(tab: Pick<EuiTabbedContentTab, "name" | "id">) => {
|
(tab: Pick<EuiTabbedContentTab, "name" | "id">) => {
|
||||||
return {
|
return {
|
||||||
...tab,
|
...tab,
|
||||||
content: getContent(tab.id),
|
content: getContent(tab.id),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
).concat([{
|
)
|
||||||
name: 'Layout',
|
let count = indexPattern?.complexFields?.length || 0
|
||||||
id: 'layout',
|
if (complexFieldFilter) {
|
||||||
content: <LayoutList indexPattern={indexPattern} clusterId={selectedCluster.id}/>,
|
const normalizedFieldFilter = complexFieldFilter.toLowerCase();
|
||||||
}]),
|
const fields = indexPattern?.complexFields?.filter((field) =>
|
||||||
[fieldFilter, getContent, indexPattern, indexPatternManagementStart.list]
|
field.name.toLowerCase().includes(normalizedFieldFilter)
|
||||||
|
);
|
||||||
|
count = fields.length
|
||||||
|
}
|
||||||
|
return tabs.concat([{
|
||||||
|
name: `Complex fields (${count})`,
|
||||||
|
id: TAB_COMPLEX_FIELDS,
|
||||||
|
content: getContent(TAB_COMPLEX_FIELDS)
|
||||||
|
}])
|
||||||
|
},
|
||||||
|
[fieldFilter, getContent, indexPattern, indexPatternManagementStart.list, complexFieldFilter]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [selectedTabId, setSelectedTabId] = useState(euiTabs[0].id);
|
const [selectedTabId, setSelectedTabId] = useState(euiTabs[0].id);
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
.editor {
|
||||||
|
background: #fff;
|
||||||
|
:global {
|
||||||
|
.euiComboBox__inputWrap .euiBadge {
|
||||||
|
background: #fcfbfd;
|
||||||
|
color: #343741;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
.euiBadge__content {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,552 @@
|
||||||
|
/*
|
||||||
|
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||||
|
* license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright
|
||||||
|
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||||
|
* the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
* not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { PureComponent, Fragment, useState, useCallback } from 'react';
|
||||||
|
import { intersection, union, get, cloneDeep } from 'lodash';
|
||||||
|
|
||||||
|
import {
|
||||||
|
EuiButton,
|
||||||
|
EuiButtonEmpty,
|
||||||
|
EuiCode,
|
||||||
|
EuiConfirmModal,
|
||||||
|
EuiFieldText,
|
||||||
|
EuiFlexGroup,
|
||||||
|
EuiFlexItem,
|
||||||
|
EuiForm,
|
||||||
|
EuiFormRow,
|
||||||
|
EuiIcon,
|
||||||
|
EuiOverlayMask,
|
||||||
|
EuiSelect,
|
||||||
|
EuiSpacer,
|
||||||
|
EuiText,
|
||||||
|
EUI_MODAL_CONFIRM_BUTTON,
|
||||||
|
EuiBadge,
|
||||||
|
} from '@elastic/eui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IndexPatternField,
|
||||||
|
FieldFormatInstanceType,
|
||||||
|
IndexPattern,
|
||||||
|
IFieldType,
|
||||||
|
KBN_FIELD_TYPES,
|
||||||
|
ES_FIELD_TYPES,
|
||||||
|
DataPublicPluginStart,
|
||||||
|
} from '../../../../../kibana/data/public';
|
||||||
|
|
||||||
|
import { FieldFormatEditor } from './components/field_format_editor';
|
||||||
|
import { IndexPatternManagmentContextValue } from '../../types';
|
||||||
|
|
||||||
|
import { FIELD_TYPES_BY_LANG, DEFAULT_FIELD_TYPES } from './constants';
|
||||||
|
|
||||||
|
// This loads Ace editor's "groovy" mode, used below to highlight the script.
|
||||||
|
import 'brace/mode/groovy';
|
||||||
|
import { useGlobalContext } from '../../context';
|
||||||
|
import { formatMessage } from "umi/locale";
|
||||||
|
import { message } from 'antd';
|
||||||
|
import functions from './functions';
|
||||||
|
import styles from './complex_field_editor.less'
|
||||||
|
import { generate20BitUUID } from '@/utils/utils';
|
||||||
|
import { Tags } from './field_editor';
|
||||||
|
|
||||||
|
const getFieldTypeFormatsList = (
|
||||||
|
field: IndexPatternField['spec'],
|
||||||
|
defaultFieldFormat: FieldFormatInstanceType,
|
||||||
|
fieldFormats: DataPublicPluginStart['fieldFormats']
|
||||||
|
) => {
|
||||||
|
const formatsByType = fieldFormats
|
||||||
|
.getByFieldType(field.type as KBN_FIELD_TYPES)
|
||||||
|
.map(({ id, title }) => ({
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
})).filter((item) => ['number', 'bytes', 'percent'].includes(item.id));
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
defaultFieldFormat,
|
||||||
|
title: '- Default -',
|
||||||
|
},
|
||||||
|
...formatsByType,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
interface FieldTypeFormat {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InitialFieldTypeFormat extends FieldTypeFormat {
|
||||||
|
defaultFieldFormat: FieldFormatInstanceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FieldEditorState {
|
||||||
|
isReady: boolean;
|
||||||
|
isCreating: boolean;
|
||||||
|
isDeprecatedLang: boolean;
|
||||||
|
fieldTypes: string[];
|
||||||
|
fieldTypeFormats: FieldTypeFormat[];
|
||||||
|
existingFieldNames: string[];
|
||||||
|
fieldFormatId?: string;
|
||||||
|
fieldFormatParams: { [key: string]: unknown };
|
||||||
|
showDeleteModal: boolean;
|
||||||
|
hasFormatError: boolean;
|
||||||
|
isSaving: boolean;
|
||||||
|
errors?: string[];
|
||||||
|
format: any;
|
||||||
|
spec: IndexPatternField['spec'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FieldEdiorProps {
|
||||||
|
indexPattern: IndexPattern;
|
||||||
|
spec: IndexPatternField['spec'];
|
||||||
|
services: {
|
||||||
|
redirectAway: () => void;
|
||||||
|
saveIndexPattern: DataPublicPluginStart['indexPatterns']['updateSavedObject'];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = useGlobalContext();
|
||||||
|
|
||||||
|
export class ComplexFieldEditor extends PureComponent<FieldEdiorProps, FieldEditorState> {
|
||||||
|
// static contextType = contextType;
|
||||||
|
|
||||||
|
public readonly context!: IndexPatternManagmentContextValue;
|
||||||
|
|
||||||
|
constructor(props: FieldEdiorProps, context: IndexPatternManagmentContextValue) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
const { spec, indexPattern } = props;
|
||||||
|
|
||||||
|
const isCreating = !indexPattern.complexFields.getByName(spec.name)
|
||||||
|
const initSpec = cloneDeep({ ...spec, type: 'number' })
|
||||||
|
if (isCreating) {
|
||||||
|
initSpec['function'] = {
|
||||||
|
'rate': {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const format = props.indexPattern.getFormatterForField(initSpec)
|
||||||
|
const DefaultFieldFormat = data.fieldFormats.getDefaultType(
|
||||||
|
initSpec.type as KBN_FIELD_TYPES,
|
||||||
|
initSpec.esTypes as ES_FIELD_TYPES[]
|
||||||
|
);
|
||||||
|
this.state = {
|
||||||
|
isDeprecatedLang: false,
|
||||||
|
existingFieldNames: indexPattern.complexFields.getAll().map((f: IFieldType) => f.name),
|
||||||
|
showDeleteModal: false,
|
||||||
|
hasFormatError: false,
|
||||||
|
isSaving: false,
|
||||||
|
format: props.indexPattern.getFormatterForField(initSpec),
|
||||||
|
spec: initSpec,
|
||||||
|
isReady: true,
|
||||||
|
isCreating: !indexPattern.complexFields.getByName(initSpec.name),
|
||||||
|
errors: [],
|
||||||
|
fieldTypeFormats: getFieldTypeFormatsList(
|
||||||
|
initSpec,
|
||||||
|
DefaultFieldFormat as FieldFormatInstanceType,
|
||||||
|
data.fieldFormats
|
||||||
|
),
|
||||||
|
fieldFormatId: get(indexPattern, ['fieldFormatMap', initSpec.name, 'type', 'id']),
|
||||||
|
fieldFormatParams: format?.params(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onFieldChange = (fieldName: string, value: string | number) => {
|
||||||
|
const { spec } = this.state;
|
||||||
|
(spec as any)[fieldName] = value;
|
||||||
|
this.forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
onFormatChange = (formatId: string, params?: any) => {
|
||||||
|
const { spec, fieldTypeFormats } = this.state;
|
||||||
|
const { uiSettings, data } = useGlobalContext(); //this.context.services;
|
||||||
|
|
||||||
|
const FieldFormat = data.fieldFormats.getType(
|
||||||
|
formatId || (fieldTypeFormats[0] as InitialFieldTypeFormat).defaultFieldFormat.id
|
||||||
|
) as FieldFormatInstanceType;
|
||||||
|
|
||||||
|
const newFormat = new FieldFormat(params, (key)=>{})//(key) => uiSettings.get(key));
|
||||||
|
spec.format = newFormat;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
fieldFormatId: FieldFormat.id,
|
||||||
|
fieldFormatParams: newFormat.params(),
|
||||||
|
format: newFormat,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onFormatParamsChange = (newParams: { fieldType: string; [key: string]: any }) => {
|
||||||
|
const { fieldFormatId } = this.state;
|
||||||
|
this.onFormatChange(fieldFormatId as string, newParams);
|
||||||
|
};
|
||||||
|
|
||||||
|
onFormatParamsError = (error?: string) => {
|
||||||
|
this.setState({
|
||||||
|
hasFormatError: !!error,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
isDuplicateName() {
|
||||||
|
const { isCreating, spec, existingFieldNames } = this.state;
|
||||||
|
return isCreating && existingFieldNames.includes(spec.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderName() {
|
||||||
|
const { isCreating, spec } = this.state;
|
||||||
|
const isInvalid = !spec.name || !spec.name.trim();
|
||||||
|
|
||||||
|
return isCreating ? (
|
||||||
|
<EuiFormRow
|
||||||
|
label={'Name'}
|
||||||
|
helpText={
|
||||||
|
this.isDuplicateName() ? (
|
||||||
|
<span>
|
||||||
|
<EuiIcon type="alert" color="warning" size="s" />
|
||||||
|
|
||||||
|
You already have a field with the name <EuiCode>{spec.name}</EuiCode>.
|
||||||
|
</span>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
isInvalid={isInvalid}
|
||||||
|
error={
|
||||||
|
isInvalid
|
||||||
|
? 'Name is required'
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<EuiFieldText
|
||||||
|
value={spec.name || ''}
|
||||||
|
placeholder={'New field'}
|
||||||
|
data-test-subj="editorFieldName"
|
||||||
|
onChange={(e) => {
|
||||||
|
this.onFieldChange('name', e.target.value);
|
||||||
|
}}
|
||||||
|
isInvalid={isInvalid}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFormat() {
|
||||||
|
const { spec, fieldTypeFormats, fieldFormatId, fieldFormatParams, format } = this.state;
|
||||||
|
const { indexPatternManagementStart } = useGlobalContext(); //this.context.services;
|
||||||
|
const defaultFormat = (fieldTypeFormats[0] as InitialFieldTypeFormat).defaultFieldFormat.title;
|
||||||
|
|
||||||
|
const label = defaultFormat ? (<>
|
||||||
|
Format (Default: <EuiCode>{defaultFormat}</EuiCode>)</>
|
||||||
|
) : (
|
||||||
|
"Format"
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<EuiFormRow
|
||||||
|
label={label}
|
||||||
|
helpText={
|
||||||
|
`Formatting allows you to control the way that specific values are displayed. It can also cause values to be
|
||||||
|
completely changed and prevent highlighting in Discover from working.`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<EuiSelect
|
||||||
|
value={fieldFormatId}
|
||||||
|
options={fieldTypeFormats.map((fmt) => {
|
||||||
|
return { value: fmt.id || '', text: fmt.title };
|
||||||
|
})}
|
||||||
|
data-test-subj="editorSelectedFormatId"
|
||||||
|
onChange={(e) => {
|
||||||
|
this.onFormatChange(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
{fieldFormatId ? (
|
||||||
|
<FieldFormatEditor
|
||||||
|
fieldType={spec.type}
|
||||||
|
fieldFormat={format}
|
||||||
|
fieldFormatId={fieldFormatId}
|
||||||
|
fieldFormatParams={fieldFormatParams}
|
||||||
|
fieldFormatEditors={indexPatternManagementStart.fieldFormatEditors}
|
||||||
|
onChange={this.onFormatParamsChange}
|
||||||
|
onError={this.onFormatParamsError}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderDeleteModal = () => {
|
||||||
|
const { spec } = this.state;
|
||||||
|
|
||||||
|
return this.state.showDeleteModal ? (
|
||||||
|
<EuiOverlayMask>
|
||||||
|
<EuiConfirmModal
|
||||||
|
title={ `Delete field '${spec.metric_name }'`}
|
||||||
|
onCancel={this.hideDeleteModal}
|
||||||
|
onConfirm={() => {
|
||||||
|
this.hideDeleteModal();
|
||||||
|
this.deleteField();
|
||||||
|
}}
|
||||||
|
cancelButtonText='Cancel'
|
||||||
|
confirmButtonText= 'Delete'
|
||||||
|
buttonColor="danger"
|
||||||
|
defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
You can't recover a deleted field. <span>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
</span>Are you sure you want to do this?
|
||||||
|
</p>
|
||||||
|
</EuiConfirmModal>
|
||||||
|
</EuiOverlayMask>
|
||||||
|
) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
showDeleteModal = () => {
|
||||||
|
this.setState({
|
||||||
|
showDeleteModal: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
hideDeleteModal = () => {
|
||||||
|
this.setState({
|
||||||
|
showDeleteModal: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
renderActions() {
|
||||||
|
const { isCreating, spec, isSaving } = this.state;
|
||||||
|
const { redirectAway } = this.props.services;
|
||||||
|
|
||||||
|
if (spec?.builtin) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiFormRow>
|
||||||
|
<EuiFlexGroup>
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiButton
|
||||||
|
fill
|
||||||
|
onClick={this.saveField}
|
||||||
|
isDisabled={this.isSavingDisabled()}
|
||||||
|
isLoading={isSaving}
|
||||||
|
data-test-subj="fieldSaveButton"
|
||||||
|
>
|
||||||
|
{isCreating ? (
|
||||||
|
"Create field"
|
||||||
|
) : (
|
||||||
|
"Save field"
|
||||||
|
)}
|
||||||
|
</EuiButton>
|
||||||
|
</EuiFlexItem>
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiButtonEmpty onClick={redirectAway} data-test-subj="fieldCancelButton">
|
||||||
|
Cancel
|
||||||
|
</EuiButtonEmpty>
|
||||||
|
</EuiFlexItem>
|
||||||
|
{!isCreating ? (
|
||||||
|
<EuiFlexItem>
|
||||||
|
<EuiFlexGroup justifyContent="flexEnd">
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiButtonEmpty color="danger" onClick={this.showDeleteModal}>
|
||||||
|
Delete
|
||||||
|
</EuiButtonEmpty>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
</EuiFlexItem>
|
||||||
|
) : null}
|
||||||
|
</EuiFlexGroup>
|
||||||
|
</EuiFormRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteField = () => {
|
||||||
|
const { redirectAway, saveIndexPattern } = this.props.services;
|
||||||
|
|
||||||
|
const field = this.state.spec;
|
||||||
|
const { indexPattern } = this.props;
|
||||||
|
|
||||||
|
const fieldExists = !!indexPattern.complexFields.getByName(field.name);
|
||||||
|
|
||||||
|
if (fieldExists) {
|
||||||
|
indexPattern.complexFields.remove(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indexPattern.fieldFormatMap[field.name]) {
|
||||||
|
indexPattern.fieldFormatMap[field.name] = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveIndexPattern(indexPattern).then(() => {
|
||||||
|
// const message = `Deleted '${spec.name}'`;
|
||||||
|
// this.context.services.notifications.toasts.addSuccess(message);
|
||||||
|
redirectAway();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
saveField = async () => {
|
||||||
|
const field = this.state.spec;
|
||||||
|
const { indexPattern } = this.props;
|
||||||
|
const { fieldFormatId } = this.state;
|
||||||
|
|
||||||
|
const { redirectAway, saveIndexPattern } = this.props.services;
|
||||||
|
const fieldExists = !!indexPattern.complexFields.getByName(field.name);
|
||||||
|
|
||||||
|
let oldField: IndexPatternField['spec'];
|
||||||
|
|
||||||
|
if (fieldExists) {
|
||||||
|
oldField = indexPattern.complexFields.getByName(field.name)!.spec;
|
||||||
|
indexPattern.complexFields.update(field);
|
||||||
|
} else {
|
||||||
|
field.name = generate20BitUUID()
|
||||||
|
indexPattern.complexFields.add(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fieldFormatId) {
|
||||||
|
indexPattern.fieldFormatMap[field.name] = undefined;
|
||||||
|
} else {
|
||||||
|
indexPattern.fieldFormatMap[field.name] = field.format;
|
||||||
|
}
|
||||||
|
|
||||||
|
return saveIndexPattern(indexPattern)
|
||||||
|
.then(() => {
|
||||||
|
// const message = i18n.translate('indexPatternManagement.deleteField.savedHeader', {
|
||||||
|
// defaultMessage: "Saved '{fieldName}'",
|
||||||
|
// values: { fieldName: field.name },
|
||||||
|
// });
|
||||||
|
// this.context.services.notifications.toasts.addSuccess(message);
|
||||||
|
redirectAway();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (oldField) {
|
||||||
|
indexPattern.complexFields.update(oldField);
|
||||||
|
} else {
|
||||||
|
indexPattern.complexFields.remove(field);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
isSavingDisabled() {
|
||||||
|
const { spec, hasFormatError } = this.state;
|
||||||
|
|
||||||
|
if (
|
||||||
|
hasFormatError
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFunction(func) {
|
||||||
|
const { spec } = this.state;
|
||||||
|
const { indexPattern } = this.props;
|
||||||
|
const component = functions[func]
|
||||||
|
const props = {
|
||||||
|
spec,
|
||||||
|
indexPattern,
|
||||||
|
onChange: (value) => {
|
||||||
|
this.onFieldChange('function', value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (component) {
|
||||||
|
return component(props)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMetricConfig() {
|
||||||
|
const { spec } = this.state;
|
||||||
|
const keys = Object.keys(spec?.function || {})
|
||||||
|
const statistic = keys[0]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EuiFormRow
|
||||||
|
label={'Metric Name'}
|
||||||
|
>
|
||||||
|
<EuiFieldText
|
||||||
|
value={spec?.metric_name}
|
||||||
|
onChange={(e) => {
|
||||||
|
this.onFieldChange('metric_name', e.target.value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
<EuiFormRow
|
||||||
|
label={'Function'}
|
||||||
|
>
|
||||||
|
<EuiSelect
|
||||||
|
options={[
|
||||||
|
"rate",
|
||||||
|
"rate_sum_func_value_in_group",
|
||||||
|
"latency",
|
||||||
|
"latency_sum_func_value_in_group",
|
||||||
|
"sum_func_value_in_group",
|
||||||
|
].map((item) => ({ value: item, text: item.toUpperCase() }))}
|
||||||
|
value={statistic}
|
||||||
|
onChange={(e) => {
|
||||||
|
this.onFieldChange('function', { [e.target.value]: {} })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
{this.renderFunction(statistic || 'rate')}
|
||||||
|
{this.renderFormat()}
|
||||||
|
<EuiFormRow
|
||||||
|
label={'Unit'}
|
||||||
|
>
|
||||||
|
<EuiFieldText
|
||||||
|
value={spec?.unit}
|
||||||
|
onChange={(e) => {
|
||||||
|
this.onFieldChange('unit', e.target.value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
<EuiFormRow
|
||||||
|
label={'Tags'}
|
||||||
|
>
|
||||||
|
<Tags value={spec?.tags} onChange={(value) => {
|
||||||
|
this.onFieldChange('tags', value)
|
||||||
|
}}/>
|
||||||
|
</EuiFormRow>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { isReady, isCreating, spec } = this.state;
|
||||||
|
|
||||||
|
return isReady ? (
|
||||||
|
<div className={styles.editor}>
|
||||||
|
<EuiText>
|
||||||
|
<h3>
|
||||||
|
{isCreating ? (
|
||||||
|
"Create field"
|
||||||
|
) : (
|
||||||
|
`Edit ${spec.metric_name }`
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
</EuiText>
|
||||||
|
<EuiSpacer size="m" />
|
||||||
|
<EuiForm>
|
||||||
|
{this.renderMetricConfig()}
|
||||||
|
{this.renderActions()}
|
||||||
|
{this.renderDeleteModal()}
|
||||||
|
</EuiForm>
|
||||||
|
<EuiSpacer size="l" />
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
.editor {
|
||||||
|
background: #fff;
|
||||||
|
:global {
|
||||||
|
.euiBadge__content {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,8 +17,8 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { PureComponent, Fragment } from 'react';
|
import React, { PureComponent, Fragment, useState, useMemo, useCallback } from 'react';
|
||||||
import { intersection, union, get } from 'lodash';
|
import { intersection, union, get, cloneDeep } from 'lodash';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EuiBasicTable,
|
EuiBasicTable,
|
||||||
|
@ -41,6 +41,13 @@ import {
|
||||||
EuiSpacer,
|
EuiSpacer,
|
||||||
EuiText,
|
EuiText,
|
||||||
EUI_MODAL_CONFIRM_BUTTON,
|
EUI_MODAL_CONFIRM_BUTTON,
|
||||||
|
EuiFilterButton,
|
||||||
|
EuiFilterGroup,
|
||||||
|
EuiPopover,
|
||||||
|
EuiSelectable,
|
||||||
|
EuiPopoverTitle,
|
||||||
|
EuiBadge,
|
||||||
|
EuiComboBox,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -69,10 +76,32 @@ import { IndexPatternManagmentContextValue } from '../../types';
|
||||||
|
|
||||||
import { FIELD_TYPES_BY_LANG, DEFAULT_FIELD_TYPES } from './constants';
|
import { FIELD_TYPES_BY_LANG, DEFAULT_FIELD_TYPES } from './constants';
|
||||||
import { executeScript, isScriptValid } from './lib';
|
import { executeScript, isScriptValid } from './lib';
|
||||||
|
import styles from './field_editor.less'
|
||||||
|
|
||||||
// This loads Ace editor's "groovy" mode, used below to highlight the script.
|
// This loads Ace editor's "groovy" mode, used below to highlight the script.
|
||||||
import 'brace/mode/groovy';
|
import 'brace/mode/groovy';
|
||||||
import { useGlobalContext } from '../../context';
|
import { useGlobalContext } from '../../context';
|
||||||
|
import { formatMessage } from "umi/locale";
|
||||||
|
import { message } from 'antd';
|
||||||
|
import { getRollupEnabled } from '@/utils/authority';
|
||||||
|
|
||||||
|
export const getStatistics = (type) => {
|
||||||
|
if (!type || type === 'string') return ["count", "cardinality"];
|
||||||
|
return [
|
||||||
|
"max",
|
||||||
|
"min",
|
||||||
|
"avg",
|
||||||
|
"sum",
|
||||||
|
"medium",
|
||||||
|
"p99",
|
||||||
|
"p95",
|
||||||
|
"p90",
|
||||||
|
"p80",
|
||||||
|
"p50",
|
||||||
|
"count",
|
||||||
|
"cardinality",
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
const getFieldTypeFormatsList = (
|
const getFieldTypeFormatsList = (
|
||||||
field: IndexPatternField['spec'],
|
field: IndexPatternField['spec'],
|
||||||
|
@ -206,7 +235,7 @@ export class FieldEditor extends PureComponent<FieldEdiorProps, FieldEditorState
|
||||||
data.fieldFormats
|
data.fieldFormats
|
||||||
),
|
),
|
||||||
fieldFormatId: get(indexPattern, ['fieldFormatMap', spec.name, 'type', 'id']),
|
fieldFormatId: get(indexPattern, ['fieldFormatMap', spec.name, 'type', 'id']),
|
||||||
fieldFormatParams: format.params(),
|
fieldFormatParams: format?.params(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -750,11 +779,72 @@ export class FieldEditor extends PureComponent<FieldEdiorProps, FieldEditorState
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMetricSettingsChange = (key, value) => {
|
||||||
|
const { spec = {} } = this.state;
|
||||||
|
const settings = cloneDeep(spec['metric_config'] || {})
|
||||||
|
settings[key] = value
|
||||||
|
this.onFieldChange('metric_config', settings)
|
||||||
|
};
|
||||||
|
|
||||||
|
renderMetricConfig() {
|
||||||
|
const { spec } = this.state;
|
||||||
|
|
||||||
|
const isRollupEnabled = getRollupEnabled() === 'true'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EuiFormRow
|
||||||
|
label={'Metric Name'}
|
||||||
|
>
|
||||||
|
<EuiFieldText
|
||||||
|
value={spec?.metric_config?.name}
|
||||||
|
onChange={(e) => {
|
||||||
|
this.onMetricSettingsChange('name', e.target.value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
<EuiFormRow
|
||||||
|
label={'Statistics'}
|
||||||
|
helpText={
|
||||||
|
isRollupEnabled ? `Rollup is enabled, some statistics will be disabled.` : ''
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StatisticsSelect
|
||||||
|
value={spec?.metric_config?.option_aggs || []}
|
||||||
|
statistics={getStatistics(spec?.type)}
|
||||||
|
onChange={(value) => {
|
||||||
|
this.onMetricSettingsChange('option_aggs', value)
|
||||||
|
}}
|
||||||
|
spec={spec}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
|
||||||
|
<EuiFormRow
|
||||||
|
label={'Unit'}
|
||||||
|
>
|
||||||
|
<EuiFieldText
|
||||||
|
value={spec?.metric_config?.unit}
|
||||||
|
onChange={(e) => {
|
||||||
|
this.onMetricSettingsChange('unit', e.target.value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
<EuiFormRow
|
||||||
|
label={'Tags'}
|
||||||
|
>
|
||||||
|
<Tags value={spec?.metric_config?.tags} onChange={(value) => {
|
||||||
|
this.onMetricSettingsChange('tags', value)
|
||||||
|
}}/>
|
||||||
|
</EuiFormRow>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isReady, isCreating, spec } = this.state;
|
const { isReady, isCreating, spec } = this.state;
|
||||||
|
|
||||||
return isReady ? (
|
return isReady ? (
|
||||||
<div>
|
<div className={styles.editor}>
|
||||||
<EuiText>
|
<EuiText>
|
||||||
<h3>
|
<h3>
|
||||||
{isCreating ? (
|
{isCreating ? (
|
||||||
|
@ -774,6 +864,7 @@ export class FieldEditor extends PureComponent<FieldEdiorProps, FieldEditorState
|
||||||
{this.renderFormat()}
|
{this.renderFormat()}
|
||||||
{this.renderPopularity()}
|
{this.renderPopularity()}
|
||||||
{this.renderScript()}
|
{this.renderScript()}
|
||||||
|
{this.renderMetricConfig()}
|
||||||
{this.renderActions()}
|
{this.renderActions()}
|
||||||
{this.renderDeleteModal()}
|
{this.renderDeleteModal()}
|
||||||
</EuiForm>
|
</EuiForm>
|
||||||
|
@ -782,3 +873,140 @@ export class FieldEditor extends PureComponent<FieldEdiorProps, FieldEditorState
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ROLLUP_FIELDS = {
|
||||||
|
"payload.elasticsearch.shard_stats.indexing.index_total": ["max", "min"],
|
||||||
|
"payload.elasticsearch.shard_stats.store.size_in_bytes": ["max", "min"],
|
||||||
|
"payload.elasticsearch.shard_stats.docs.count": ["max", "min"],
|
||||||
|
"payload.elasticsearch.shard_stats.search.query_total":["max", "min"],
|
||||||
|
"payload.elasticsearch.shard_stats.indexing.index_time_in_millis": ["max", "min"],
|
||||||
|
"payload.elasticsearch.shard_stats.search.query_time_in_millis": ["max", "min"],
|
||||||
|
"payload.elasticsearch.shard_stats.segments.count": ["max", "min"],
|
||||||
|
"payload.elasticsearch.shard_stats.segments.memory_in_bytes": ["max", "min"],
|
||||||
|
"payload.elasticsearch.index_stats.total.store.size_in_bytes": ["max", "min"],
|
||||||
|
"payload.elasticsearch.index_stats.total.docs.count": ["max", "min"],
|
||||||
|
"payload.elasticsearch.index_stats.total.search.query_total": ["max", "min"],
|
||||||
|
"payload.elasticsearch.index_stats.primaries.indexing.index_total": ["max", "min"],
|
||||||
|
"payload.elasticsearch.index_stats.primaries.indexing.index_time_in_millis": ["max", "min"],
|
||||||
|
"payload.elasticsearch.index_stats.total.search.query_time_in_millis": ["max", "min"],
|
||||||
|
"payload.elasticsearch.index_stats.total.segments.count": ["max", "min"],
|
||||||
|
"payload.elasticsearch.index_stats.total.segments.memory_in_bytes": ["max", "min"],
|
||||||
|
"payload.elasticsearch.node_stats.indices.indexing.index_total": ["max", "min"],
|
||||||
|
"payload.elasticsearch.node_stats.process.cpu.percent": ["p99", "max", "medium", "min", "avg"],
|
||||||
|
"payload.elasticsearch.node_stats.jvm.mem.heap_used_in_bytes": ["p99", "max", "medium", "min", "avg"],
|
||||||
|
"payload.elasticsearch.node_stats.indices.indexing.index_time_in_millis": ["max", "min"],
|
||||||
|
"payload.elasticsearch.node_stats.indices.search.query_total": ["max", "min"],
|
||||||
|
"payload.elasticsearch.node_stats.indices.search.query_time_in_millis": ["max", "min"],
|
||||||
|
"payload.elasticsearch.node_stats.indices.store.size_in_bytes": ["max", "min"],
|
||||||
|
"payload.elasticsearch.node_stats.indices.docs.count": ["max", "min"]
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatisticsSelect = (props) => {
|
||||||
|
|
||||||
|
const { value = [], statistics = [], onChange, spec } = props;
|
||||||
|
|
||||||
|
const isRollupEnabled = getRollupEnabled() === 'true'
|
||||||
|
|
||||||
|
const options = useMemo(() => {
|
||||||
|
const limits = ROLLUP_FIELDS[spec?.name] || ['max', 'count']
|
||||||
|
return statistics.map((item) => ({
|
||||||
|
label: item.toUpperCase(), value: item, disabled: isRollupEnabled ? !limits.includes(item) : false
|
||||||
|
}))
|
||||||
|
}, [value, statistics, spec, isRollupEnabled])
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiComboBox
|
||||||
|
options={options}
|
||||||
|
selectedOptions={value.map((item) => ({ label: item.toUpperCase(), value: item }))}
|
||||||
|
onChange={(value) => {
|
||||||
|
onChange(value.map((item) => item.value))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Tags = ({ value = [], onChange }) => {
|
||||||
|
const [inputVisible, setInputVisible] = useState(false);
|
||||||
|
const [inputValue, setInputValue] = useState("");
|
||||||
|
|
||||||
|
const handleInputChange = (e) => {
|
||||||
|
setInputValue(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showInput = () => {
|
||||||
|
setInputVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = useCallback(
|
||||||
|
(index) => {
|
||||||
|
const newValue = [...value];
|
||||||
|
newValue.splice(index, 1);
|
||||||
|
onChange(newValue);
|
||||||
|
},
|
||||||
|
[value]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleInputConfirm = (input) => {
|
||||||
|
if (input.length === 0) {
|
||||||
|
return message.warning(
|
||||||
|
formatMessage({ id: "command.message.invalid.tag" })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (input) onChange([...(value || []), input]);
|
||||||
|
setInputVisible(false);
|
||||||
|
setInputValue("");
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
handleInputConfirm(inputValue)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiFlexGroup wrap responsive={false} gutterSize="xs">
|
||||||
|
{value.map((tag, index) => (
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiBadge
|
||||||
|
color="hollow"
|
||||||
|
iconType="cross"
|
||||||
|
iconSide="right"
|
||||||
|
iconOnClick={() => handleRemove(index)}
|
||||||
|
style={{ height: '40px', lineHeight: '40px', fontSize: 14}}
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</EuiBadge>
|
||||||
|
</EuiFlexItem>
|
||||||
|
|
||||||
|
))}
|
||||||
|
{inputVisible && (
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiFieldText
|
||||||
|
value={inputValue}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onBlur={() => {
|
||||||
|
setInputVisible(false);
|
||||||
|
setInputValue("");
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</EuiFlexItem>
|
||||||
|
)}
|
||||||
|
{!inputVisible && (
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiBadge
|
||||||
|
color="hollow"
|
||||||
|
iconType="plus"
|
||||||
|
iconSide="left"
|
||||||
|
onClick={showInput}
|
||||||
|
style={{ height: '40px', lineHeight: '40px', fontSize: 14}}
|
||||||
|
>
|
||||||
|
Add New
|
||||||
|
</EuiBadge>
|
||||||
|
</EuiFlexItem>
|
||||||
|
)}
|
||||||
|
</EuiFlexGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import rate from './rate'
|
||||||
|
import rate_sum_func_value_in_group from './rate_sum_func_value_in_group'
|
||||||
|
import latency from './latency'
|
||||||
|
import latency_sum_func_value_in_group from './latency_sum_func_value_in_group'
|
||||||
|
import sum_func_value_in_group from './sum_func_value_in_group'
|
||||||
|
|
||||||
|
export const getStatistics = (type) => {
|
||||||
|
if (type !== 'string') {
|
||||||
|
return [
|
||||||
|
"max",
|
||||||
|
"min",
|
||||||
|
"avg",
|
||||||
|
"sum",
|
||||||
|
"medium",
|
||||||
|
"p99",
|
||||||
|
"p95",
|
||||||
|
"p90",
|
||||||
|
"p80",
|
||||||
|
"p50",
|
||||||
|
"count",
|
||||||
|
"cardinality",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return ["count", "cardinality"];
|
||||||
|
};
|
||||||
|
|
||||||
|
const functions = {
|
||||||
|
rate,
|
||||||
|
rate_sum_func_value_in_group,
|
||||||
|
latency,
|
||||||
|
latency_sum_func_value_in_group,
|
||||||
|
sum_func_value_in_group,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default functions
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { EuiComboBox, EuiFormRow } from "@elastic/eui"
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
const { indexPattern, spec, onChange } = props;
|
||||||
|
const keys = Object.keys(spec?.function || {})
|
||||||
|
const statistic = keys[0]
|
||||||
|
const func = spec?.function?.[statistic]
|
||||||
|
const { divisor, dividend } = func || {}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EuiFormRow label={'Dividend Field'} >
|
||||||
|
<EuiComboBox
|
||||||
|
singleSelection
|
||||||
|
options={indexPattern.fields?.filter((item) => !!item.spec?.name).map((item) => (
|
||||||
|
{ value: item.spec?.name, label: item.spec?.name }
|
||||||
|
))}
|
||||||
|
selectedOptions={dividend ? [{ value: dividend, label: dividend }] : []}
|
||||||
|
onChange={(value) => {
|
||||||
|
onChange({ [statistic]: {
|
||||||
|
...(func || {}),
|
||||||
|
dividend: value[0]?.value
|
||||||
|
}})
|
||||||
|
}}
|
||||||
|
isClearable={false}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
<EuiFormRow label={'Divisor Field'} >
|
||||||
|
<EuiComboBox
|
||||||
|
singleSelection
|
||||||
|
options={indexPattern.fields?.filter((item) => !!item.spec?.name).map((item) => (
|
||||||
|
{ value: item.spec?.name, label: item.spec?.name }
|
||||||
|
))}
|
||||||
|
selectedOptions={divisor ? [{ value: divisor, label: divisor }] : []}
|
||||||
|
onChange={(value) => {
|
||||||
|
onChange({ [statistic]: {
|
||||||
|
...(func || {}),
|
||||||
|
divisor: value[0]?.value
|
||||||
|
}})
|
||||||
|
}}
|
||||||
|
isClearable={false}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { EuiComboBox, EuiFormRow } from "@elastic/eui"
|
||||||
|
import { getStatistics } from ".";
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
const { indexPattern, spec, onChange } = props;
|
||||||
|
const keys = Object.keys(spec?.function || {})
|
||||||
|
const statistic = keys[0]
|
||||||
|
const func = spec?.function?.[statistic]
|
||||||
|
const { divisor, dividend, group } = func || {}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EuiFormRow label={'Dividend Field'} >
|
||||||
|
<EuiComboBox
|
||||||
|
singleSelection
|
||||||
|
options={indexPattern.fields?.filter((item) => !!item.spec?.name).map((item) => (
|
||||||
|
{ value: item.spec?.name, label: item.spec?.name, type: item.spec?.type }
|
||||||
|
))}
|
||||||
|
selectedOptions={dividend ? [{ value: dividend, label: dividend }] : []}
|
||||||
|
onChange={(value) => {
|
||||||
|
const types = getStatistics(value[0]?.type)
|
||||||
|
onChange({ [statistic]: {
|
||||||
|
...(func || {}),
|
||||||
|
dividend: value[0]?.value,
|
||||||
|
group: {
|
||||||
|
...(func?.group || {}),
|
||||||
|
func: types.includes('max') ? 'max' : types[0]
|
||||||
|
}
|
||||||
|
}})
|
||||||
|
}}
|
||||||
|
isClearable={false}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
<EuiFormRow label={'Divisor Field'} >
|
||||||
|
<EuiComboBox
|
||||||
|
singleSelection
|
||||||
|
options={indexPattern.fields?.filter((item) => !!item.spec?.name).map((item) => (
|
||||||
|
{ value: item.spec?.name, label: item.spec?.name }
|
||||||
|
))}
|
||||||
|
selectedOptions={divisor ? [{ value: divisor, label: divisor }] : []}
|
||||||
|
onChange={(value) => {
|
||||||
|
onChange({ [statistic]: {
|
||||||
|
...(func || {}),
|
||||||
|
divisor: value[0]?.value
|
||||||
|
}})
|
||||||
|
}}
|
||||||
|
isClearable={false}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
<EuiFormRow label={'Group Field'} >
|
||||||
|
<EuiComboBox
|
||||||
|
singleSelection
|
||||||
|
options={indexPattern.fields?.filter((item) => !!item.spec?.name).map((item) => (
|
||||||
|
{ value: item.spec?.name, label: item.spec?.name }
|
||||||
|
))}
|
||||||
|
selectedOptions={group?.field ? [{ value: group?.field, label: group?.field }] : []}
|
||||||
|
onChange={(value) => {
|
||||||
|
onChange({ [statistic]: {
|
||||||
|
...(func || {}),
|
||||||
|
group: {
|
||||||
|
...(func?.group || {}),
|
||||||
|
field: value[0]?.value,
|
||||||
|
}
|
||||||
|
}})
|
||||||
|
}}
|
||||||
|
isClearable={false}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
{/* <EuiFormRow label={'Group Statistic'} >
|
||||||
|
<EuiComboBox
|
||||||
|
singleSelection
|
||||||
|
options={getStatistics(group?.type).map((item) => (
|
||||||
|
{ value: item, label: item.toUpperCase() }
|
||||||
|
))}
|
||||||
|
selectedOptions={group?.func ? [{ value: group?.func, label: group?.func?.toUpperCase() }] : []}
|
||||||
|
onChange={(value) => {
|
||||||
|
onChange({ [statistic]: {
|
||||||
|
...(func || {}),
|
||||||
|
group: {
|
||||||
|
...(group || {}),
|
||||||
|
func: value[0]?.value
|
||||||
|
}
|
||||||
|
}})
|
||||||
|
}}
|
||||||
|
isClearable={false}
|
||||||
|
/>
|
||||||
|
</EuiFormRow> */}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { EuiComboBox, EuiFormRow } from "@elastic/eui"
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
const { indexPattern, spec, onChange } = props;
|
||||||
|
const keys = Object.keys(spec?.function || {})
|
||||||
|
const statistic = keys[0]
|
||||||
|
const func = spec?.function?.[statistic]
|
||||||
|
const { field, group } = func || {}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiFormRow label={'Field'} >
|
||||||
|
<EuiComboBox
|
||||||
|
singleSelection
|
||||||
|
options={indexPattern.fields?.filter((item) => !!item.spec?.name).map((item) => (
|
||||||
|
{ value: item.spec?.name, label: item.spec?.name }
|
||||||
|
))}
|
||||||
|
selectedOptions={field ? [{ value: field, label: field }] : []}
|
||||||
|
onChange={(value) => {
|
||||||
|
onChange({ [statistic]: {
|
||||||
|
...(func || {}),
|
||||||
|
field: value[0]?.value
|
||||||
|
}})
|
||||||
|
}}
|
||||||
|
isClearable={false}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { EuiComboBox, EuiFormRow } from "@elastic/eui"
|
||||||
|
import { getStatistics } from ".";
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
const { indexPattern, spec, onChange } = props;
|
||||||
|
const keys = Object.keys(spec?.function || {})
|
||||||
|
const statistic = keys[0]
|
||||||
|
const func = spec?.function?.[statistic]
|
||||||
|
const { field, group } = func || {}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EuiFormRow label={'Field'} >
|
||||||
|
<EuiComboBox
|
||||||
|
singleSelection
|
||||||
|
options={indexPattern.fields?.filter((item) => !!item.spec?.name).map((item) => (
|
||||||
|
{ value: item.spec?.name, label: item.spec?.name, type: item.spec?.type }
|
||||||
|
))}
|
||||||
|
selectedOptions={field ? [{ value: field, label: field }] : []}
|
||||||
|
onChange={(value) => {
|
||||||
|
const types = getStatistics(value[0]?.type)
|
||||||
|
onChange({ [statistic]: {
|
||||||
|
...(func || {}),
|
||||||
|
field: value[0]?.value,
|
||||||
|
group: {
|
||||||
|
func: types.includes('max') ? 'max' : types[0]
|
||||||
|
}
|
||||||
|
}})
|
||||||
|
}}
|
||||||
|
isClearable={false}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
<EuiFormRow label={'Group Field'} >
|
||||||
|
<EuiComboBox
|
||||||
|
singleSelection
|
||||||
|
options={indexPattern.fields?.filter((item) => !!item.spec?.name).map((item) => (
|
||||||
|
{ value: item.spec?.name, label: item.spec?.name }
|
||||||
|
))}
|
||||||
|
selectedOptions={group?.field ? [{ value: group?.field, label: group?.field }] : []}
|
||||||
|
onChange={(value) => {
|
||||||
|
onChange({ [statistic]: {
|
||||||
|
...(func || {}),
|
||||||
|
group: {
|
||||||
|
...(func?.group || {}),
|
||||||
|
field: value[0]?.value,
|
||||||
|
}
|
||||||
|
}})
|
||||||
|
}}
|
||||||
|
isClearable={false}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
{/* <EuiFormRow label={'Group Statistic'} >
|
||||||
|
<EuiComboBox
|
||||||
|
singleSelection
|
||||||
|
options={getStatistics(group?.type).map((item) => (
|
||||||
|
{ value: item, label: item.toUpperCase() }
|
||||||
|
))}
|
||||||
|
selectedOptions={group?.func ? [{ value: group?.func, label: group?.func?.toUpperCase() }] : []}
|
||||||
|
onChange={(value) => {
|
||||||
|
onChange({ [statistic]: {
|
||||||
|
...(func || {}),
|
||||||
|
group: {
|
||||||
|
...(group || {}),
|
||||||
|
func: value[0]?.value
|
||||||
|
}
|
||||||
|
}})
|
||||||
|
}}
|
||||||
|
isClearable={false}
|
||||||
|
/>
|
||||||
|
</EuiFormRow> */}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { EuiComboBox, EuiFormRow } from "@elastic/eui"
|
||||||
|
import { useState } from "react";
|
||||||
|
import { getStatistics } from ".";
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
const { indexPattern, spec, onChange } = props;
|
||||||
|
const keys = Object.keys(spec?.function || {})
|
||||||
|
const statistic = keys[0]
|
||||||
|
const func = spec?.function?.[statistic]
|
||||||
|
const { field, group } = func || {}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EuiFormRow label={'Field'} >
|
||||||
|
<EuiComboBox
|
||||||
|
singleSelection
|
||||||
|
options={indexPattern.fields?.filter((item) => !!item.spec?.name).map((item) => (
|
||||||
|
{ value: item.spec?.name, label: item.spec?.name, type: item.spec?.type }
|
||||||
|
))}
|
||||||
|
selectedOptions={field ? [{ value: field, label: field }] : []}
|
||||||
|
onChange={(value) => {
|
||||||
|
const types = getStatistics(value[0]?.type)
|
||||||
|
onChange({ [statistic]: {
|
||||||
|
...(func || {}),
|
||||||
|
field: value[0]?.value,
|
||||||
|
group: {
|
||||||
|
func: types.includes('max') ? 'max' : types[0]
|
||||||
|
}
|
||||||
|
}})
|
||||||
|
}}
|
||||||
|
isClearable={false}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
<EuiFormRow label={'Group Field'} >
|
||||||
|
<EuiComboBox
|
||||||
|
singleSelection
|
||||||
|
options={indexPattern.fields?.filter((item) => !!item.spec?.name).map((item) => (
|
||||||
|
{ value: item.spec?.name, label: item.spec?.name }
|
||||||
|
))}
|
||||||
|
selectedOptions={group?.field ? [{ value: group?.field, label: group?.field }] : []}
|
||||||
|
onChange={(value) => {
|
||||||
|
onChange({ [statistic]: {
|
||||||
|
...(func || {}),
|
||||||
|
group: {
|
||||||
|
...(func?.group || {}),
|
||||||
|
field: value[0]?.value,
|
||||||
|
}
|
||||||
|
}})
|
||||||
|
}}
|
||||||
|
isClearable={false}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
{/* <EuiFormRow label={'Group Statistic'} >
|
||||||
|
<EuiComboBox
|
||||||
|
singleSelection
|
||||||
|
options={getStatistics(group?.type).map((item) => (
|
||||||
|
{ value: item, label: item.toUpperCase() }
|
||||||
|
))}
|
||||||
|
selectedOptions={group?.func ? [{ value: group?.func, label: group?.func?.toUpperCase() }] : []}
|
||||||
|
onChange={(value) => {
|
||||||
|
onChange({ [statistic]: {
|
||||||
|
...(func || {}),
|
||||||
|
group: {
|
||||||
|
...(group || {}),
|
||||||
|
func: value[0]?.value
|
||||||
|
}
|
||||||
|
}})
|
||||||
|
}}
|
||||||
|
isClearable={false}
|
||||||
|
/>
|
||||||
|
</EuiFormRow> */}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -81,6 +81,7 @@ export default {
|
||||||
"form.publicUsers.option.B": "Colleague B",
|
"form.publicUsers.option.B": "Colleague B",
|
||||||
"form.publicUsers.option.C": "Colleague C",
|
"form.publicUsers.option.C": "Colleague C",
|
||||||
"form.button.search": "Search",
|
"form.button.search": "Search",
|
||||||
|
"form.button.apply": "Apply",
|
||||||
"form.button.new": "New",
|
"form.button.new": "New",
|
||||||
"form.button.create": "Create",
|
"form.button.create": "Create",
|
||||||
"form.button.add": "Add",
|
"form.button.add": "Add",
|
||||||
|
|
|
@ -119,6 +119,10 @@ export default {
|
||||||
"cluster.monitor.timepicker.lastyear": "Last year",
|
"cluster.monitor.timepicker.lastyear": "Last year",
|
||||||
"cluster.monitor.timepicker.today": "Today",
|
"cluster.monitor.timepicker.today": "Today",
|
||||||
|
|
||||||
|
"cluster.monitor.topn.area": "Area Metric",
|
||||||
|
"cluster.monitor.topn.color": "Color Metric",
|
||||||
|
"cluster.monitor.topn.theme": "Theme",
|
||||||
|
|
||||||
"cluster.metrics.axis.index_throughput.title": "Indexing Rate",
|
"cluster.metrics.axis.index_throughput.title": "Indexing Rate",
|
||||||
"cluster.metrics.axis.search_throughput.title": "Search Rate",
|
"cluster.metrics.axis.search_throughput.title": "Search Rate",
|
||||||
"cluster.metrics.axis.index_latency.title": "Indexing Latency",
|
"cluster.metrics.axis.index_latency.title": "Indexing Latency",
|
||||||
|
|
|
@ -86,6 +86,7 @@ export default {
|
||||||
"form.logstash.kafkaconf.label": "Logstash Kafka 配置",
|
"form.logstash.kafkaconf.label": "Logstash Kafka 配置",
|
||||||
"form.logstash.kafkaconf.placeholder": "请输入Kafka配置",
|
"form.logstash.kafkaconf.placeholder": "请输入Kafka配置",
|
||||||
"form.button.search": "搜索",
|
"form.button.search": "搜索",
|
||||||
|
"form.button.apply": "应用",
|
||||||
"form.button.new": "新建",
|
"form.button.new": "新建",
|
||||||
"form.button.create": "创建",
|
"form.button.create": "创建",
|
||||||
"form.button.add": "添加",
|
"form.button.add": "添加",
|
||||||
|
|
|
@ -110,6 +110,10 @@ export default {
|
||||||
"cluster.monitor.timepicker.lastyear": "最近1年",
|
"cluster.monitor.timepicker.lastyear": "最近1年",
|
||||||
"cluster.monitor.timepicker.today": "今天",
|
"cluster.monitor.timepicker.today": "今天",
|
||||||
|
|
||||||
|
"cluster.monitor.topn.area": "面积指标",
|
||||||
|
"cluster.monitor.topn.color": "颜色指标",
|
||||||
|
"cluster.monitor.topn.theme": "主题",
|
||||||
|
|
||||||
"cluster.metrics.axis.index_throughput.title": "索引吞吐",
|
"cluster.metrics.axis.index_throughput.title": "索引吞吐",
|
||||||
"cluster.metrics.axis.search_throughput.title": "查询吞吐",
|
"cluster.metrics.axis.search_throughput.title": "查询吞吐",
|
||||||
"cluster.metrics.axis.index_latency.title": "索引延迟",
|
"cluster.metrics.axis.index_latency.title": "索引延迟",
|
||||||
|
|
|
@ -112,15 +112,15 @@ const Discover = (props) => {
|
||||||
|
|
||||||
const [timeZone, setTimeZone] = useState(() => getTimezone());
|
const [timeZone, setTimeZone] = useState(() => getTimezone());
|
||||||
const [mode, setMode] = useState("table");
|
const [mode, setMode] = useState("table");
|
||||||
const [viewLayout, setViewLayout] = useState();
|
// const [viewLayout, setViewLayout] = useState();
|
||||||
const insightBarRef = useRef();
|
const insightBarRef = useRef();
|
||||||
const visRef = useRef();
|
const visRef = useRef();
|
||||||
const layoutRef = useRef();
|
// const layoutRef = useRef();
|
||||||
const rangeCacheRef = useRef();
|
const rangeCacheRef = useRef();
|
||||||
const fullScreenHandle = useFullScreenHandle();
|
const fullScreenHandle = useFullScreenHandle();
|
||||||
const [layout, setLayout] = useState(
|
// const [layout, setLayout] = useState(
|
||||||
Layouts.find((item) => item.name === "default")
|
// Layouts.find((item) => item.name === "default")
|
||||||
);
|
// );
|
||||||
const [timeTipsLoading, setTimeTipsLoading] = useState(false);
|
const [timeTipsLoading, setTimeTipsLoading] = useState(false);
|
||||||
const [insightLoading, setInsightLoading] = useState(false);
|
const [insightLoading, setInsightLoading] = useState(false);
|
||||||
const [showResultCount, setShowResultCount] = useState(true);
|
const [showResultCount, setShowResultCount] = useState(true);
|
||||||
|
@ -293,9 +293,9 @@ const Discover = (props) => {
|
||||||
getFilters()
|
getFilters()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (mode === "layout") {
|
// if (mode === "layout") {
|
||||||
layoutRef?.current?.onRefresh();
|
// layoutRef?.current?.onRefresh();
|
||||||
}
|
// }
|
||||||
if (!indexPatternRef.current) {
|
if (!indexPatternRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -415,7 +415,7 @@ const Discover = (props) => {
|
||||||
columns: record.filter?.columns || ["_source"],
|
columns: record.filter?.columns || ["_source"],
|
||||||
}
|
}
|
||||||
if (record.time_field) {
|
if (record.time_field) {
|
||||||
newState.sort = [{[record.time_field]: {order: "desc"}}]
|
newState.sort = [[record.time_field, 'desc']]
|
||||||
}
|
}
|
||||||
setState(newState);
|
setState(newState);
|
||||||
if (record.filter?.filters?.length > 0) {
|
if (record.filter?.filters?.length > 0) {
|
||||||
|
@ -881,24 +881,24 @@ const Discover = (props) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchViewDefaultLayout = async (clusterId, viewId) => {
|
// const fetchViewDefaultLayout = async (clusterId, viewId) => {
|
||||||
setInsightLoading(true);
|
// setInsightLoading(true);
|
||||||
const res = await request(
|
// const res = await request(
|
||||||
`/elasticsearch/${clusterId}/saved_objects/view/${viewId}`
|
// `/elasticsearch/${clusterId}/saved_objects/view/${viewId}`
|
||||||
);
|
// );
|
||||||
const layoutId = res?._source?.default_layout_id;
|
// const layoutId = res?._source?.default_layout_id;
|
||||||
if (layoutId) {
|
// if (layoutId) {
|
||||||
const layout = await request(`/layout/${layoutId}`);
|
// const layout = await request(`/layout/${layoutId}`);
|
||||||
if (layout?._source) {
|
// if (layout?._source) {
|
||||||
setViewLayout(layout?._source);
|
// setViewLayout(layout?._source);
|
||||||
} else {
|
// } else {
|
||||||
setViewLayout();
|
// setViewLayout();
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
setViewLayout();
|
// setViewLayout();
|
||||||
}
|
// }
|
||||||
setInsightLoading(false);
|
// setInsightLoading(false);
|
||||||
};
|
// };
|
||||||
|
|
||||||
const onFieldAgg = async (field, beforeFuc, afterFuc) => {
|
const onFieldAgg = async (field, beforeFuc, afterFuc) => {
|
||||||
let name = field?.spec?.name || field?.name
|
let name = field?.spec?.name || field?.name
|
||||||
|
@ -993,27 +993,27 @@ const Discover = (props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
if (indexPattern?.type === "view") {
|
// if (indexPattern?.type === "view") {
|
||||||
fetchViewDefaultLayout(props.selectedCluster?.id, indexPattern?.id);
|
// fetchViewDefaultLayout(props.selectedCluster?.id, indexPattern?.id);
|
||||||
} else {
|
// } else {
|
||||||
setViewLayout();
|
// setViewLayout();
|
||||||
}
|
// }
|
||||||
}, [indexPattern, props.selectedCluster?.id]);
|
// }, [indexPattern, props.selectedCluster?.id]);
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
setMode(viewLayout ? "layout" : "table");
|
// setMode(viewLayout ? "layout" : "table");
|
||||||
}, [viewLayout]);
|
// }, [viewLayout]);
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
if (mode === "table" && viewLayout) {
|
// if (mode === "table" && viewLayout) {
|
||||||
setViewLayout();
|
// setViewLayout();
|
||||||
}
|
// }
|
||||||
}, [mode]);
|
// }, [mode]);
|
||||||
|
|
||||||
const showLayoutListIcon = useMemo(() => {
|
// const showLayoutListIcon = useMemo(() => {
|
||||||
return indexPattern?.type === "view";
|
// return indexPattern?.type === "view";
|
||||||
}, [indexPattern]);
|
// }, [indexPattern]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: "relative" }}>
|
<div style={{ position: "relative" }}>
|
||||||
|
@ -1104,10 +1104,10 @@ const Discover = (props) => {
|
||||||
getBucketSize,
|
getBucketSize,
|
||||||
columns: state.columns,
|
columns: state.columns,
|
||||||
}}
|
}}
|
||||||
layoutConfig={{
|
// layoutConfig={{
|
||||||
layout,
|
// layout,
|
||||||
onChange: setLayout,
|
// onChange: setLayout,
|
||||||
}}
|
// }}
|
||||||
isEmpty={resultState === "none" && queryFrom === 0}
|
isEmpty={resultState === "none" && queryFrom === 0}
|
||||||
onQueriesSelect={onQueriesSelect}
|
onQueriesSelect={onQueriesSelect}
|
||||||
onQueriesRemove={(id) => {
|
onQueriesRemove={(id) => {
|
||||||
|
@ -1159,15 +1159,15 @@ const Discover = (props) => {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
showLayoutListIcon={showLayoutListIcon}
|
showLayoutListIcon={false}
|
||||||
viewLayout={viewLayout}
|
// viewLayout={viewLayout}
|
||||||
onViewLayoutChange={(layout) => {
|
// onViewLayoutChange={(layout) => {
|
||||||
if (layout) {
|
// if (layout) {
|
||||||
setViewLayout(layout);
|
// setViewLayout(layout);
|
||||||
} else {
|
// } else {
|
||||||
setViewLayout();
|
// setViewLayout();
|
||||||
}
|
// }
|
||||||
}}
|
// }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1200,20 +1200,22 @@ const Discover = (props) => {
|
||||||
getFilters={getSearchFilters}
|
getFilters={getSearchFilters}
|
||||||
getBucketSize={getBucketSize}
|
getBucketSize={getBucketSize}
|
||||||
fullScreenHandle={fullScreenHandle}
|
fullScreenHandle={fullScreenHandle}
|
||||||
layout={layout}
|
// layout={layout}
|
||||||
selectedQueries={selectedQueries}
|
selectedQueries={selectedQueries}
|
||||||
/>
|
/>
|
||||||
) : mode === "layout" ? (
|
) :
|
||||||
<Layout
|
// mode === "layout" ? (
|
||||||
ref={layoutRef}
|
// <Layout
|
||||||
clusterId={props.selectedCluster?.id}
|
// ref={layoutRef}
|
||||||
indexPattern={indexPattern}
|
// clusterId={props.selectedCluster?.id}
|
||||||
timeRange={timefilter?.getTime()}
|
// indexPattern={indexPattern}
|
||||||
query={getSearchFilters()}
|
// timeRange={timefilter?.getTime()}
|
||||||
layout={viewLayout}
|
// query={getSearchFilters()}
|
||||||
fullScreenHandle={fullScreenHandle}
|
// layout={viewLayout}
|
||||||
/>
|
// fullScreenHandle={fullScreenHandle}
|
||||||
) : (
|
// />
|
||||||
|
// ) :
|
||||||
|
(
|
||||||
<>
|
<>
|
||||||
{indexPattern && (
|
{indexPattern && (
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { formatMessage } from "umi/locale";
|
||||||
import { getAuthority, hasAuthority } from "@/utils/authority";
|
import { getAuthority, hasAuthority } from "@/utils/authority";
|
||||||
import EditLayout from "./View/EditLayout";
|
import EditLayout from "./View/EditLayout";
|
||||||
import { Card, Empty } from "antd";
|
import { Card, Empty } from "antd";
|
||||||
|
import { CreateEditComplexFieldContainer } from "@/components/vendor/index_pattern_management/public/components/edit_index_pattern/create_edit_complex_field";
|
||||||
|
|
||||||
const IndexPatterns = (props) => {
|
const IndexPatterns = (props) => {
|
||||||
if (!props.selectedCluster?.id) {
|
if (!props.selectedCluster?.id) {
|
||||||
|
@ -57,15 +58,11 @@ const IndexPatterns = (props) => {
|
||||||
</Route>
|
</Route>
|
||||||
<Route
|
<Route
|
||||||
path={[
|
path={[
|
||||||
"/patterns/:id/layout/create",
|
"/patterns/:id/complex/:fieldName/edit",
|
||||||
"/patterns/:id/layout/:layoutId/edit",
|
"/patterns/:id/complex/create/",
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<EditLayout
|
<CreateEditComplexFieldContainer selectedCluster={props.selectedCluster} />
|
||||||
selectedCluster={props.selectedCluster}
|
|
||||||
clusterList={props.clusterList}
|
|
||||||
clusterStatus={props.clusterStatus}
|
|
||||||
/>
|
|
||||||
</Route>
|
</Route>
|
||||||
<Route
|
<Route
|
||||||
path={[
|
path={[
|
||||||
|
|
|
@ -38,18 +38,14 @@ export default (props) => {
|
||||||
>
|
>
|
||||||
<TopN type={param?.tab} {...props}/>
|
<TopN type={param?.tab} {...props}/>
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
{
|
<Tabs.TabPane
|
||||||
!isAgent && (
|
key="index"
|
||||||
<Tabs.TabPane
|
tab={formatMessage({
|
||||||
key="index"
|
id: "cluster.monitor.index.title",
|
||||||
tab={formatMessage({
|
})}
|
||||||
id: "cluster.monitor.index.title",
|
>
|
||||||
})}
|
<TopN type={param?.tab} {...props}/>
|
||||||
>
|
</Tabs.TabPane>
|
||||||
<TopN type={param?.tab} {...props}/>
|
|
||||||
</Tabs.TabPane>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
isAgent && (
|
isAgent && (
|
||||||
<Tabs.TabPane
|
<Tabs.TabPane
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { getFormatter } from "@/utils/format";
|
||||||
|
import { Empty } from "antd";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import Heatmap from "./Heatmap";
|
||||||
|
import Treemap from "./Treemap";
|
||||||
|
|
||||||
|
const generateGradientColors = (startColor, endColor, steps) => {
|
||||||
|
function colorToRgb(color) {
|
||||||
|
const rgb = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(color);
|
||||||
|
return rgb ? {
|
||||||
|
r: parseInt(rgb[1], 16),
|
||||||
|
g: parseInt(rgb[2], 16),
|
||||||
|
b: parseInt(rgb[3], 16)
|
||||||
|
} : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rgbToHex(r, g, b) {
|
||||||
|
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
const startRGB = colorToRgb(startColor);
|
||||||
|
const endRGB = colorToRgb(endColor);
|
||||||
|
const diffR = endRGB.r - startRGB.r;
|
||||||
|
const diffG = endRGB.g - startRGB.g;
|
||||||
|
const diffB = endRGB.b - startRGB.b;
|
||||||
|
|
||||||
|
const colors = [];
|
||||||
|
for (let i = 0; i <= steps; i++) {
|
||||||
|
const r = startRGB.r + (diffR * i / steps);
|
||||||
|
const g = startRGB.g + (diffG * i / steps);
|
||||||
|
const b = startRGB.b + (diffB * i / steps);
|
||||||
|
colors.push(rgbToHex(Math.round(r), Math.round(g), Math.round(b)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateColors = (colors, data) => {
|
||||||
|
if (!colors || colors.length <= 1 || !data || data.length <= 1 || data.length <= colors.length) return colors
|
||||||
|
const gradientSize = data.length - colors.length
|
||||||
|
const steps = Math.floor(gradientSize / (colors.length - 1)) + 1
|
||||||
|
let remainder = gradientSize % (colors.length - 1)
|
||||||
|
const newColors = []
|
||||||
|
for(let i=0; i<colors.length - 1; i++) {
|
||||||
|
let fixSteps = steps;
|
||||||
|
if (remainder > 0) {
|
||||||
|
fixSteps++
|
||||||
|
remainder--
|
||||||
|
}
|
||||||
|
const gradientColors = generateGradientColors(colors[i], colors[i+1], fixSteps)
|
||||||
|
newColors.push(...gradientColors.slice(0, gradientColors.length - 1))
|
||||||
|
}
|
||||||
|
newColors.push(colors[colors.length - 1])
|
||||||
|
return newColors
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fixFormatter = (formatType, pattern = '0,0.[00]a') => {
|
||||||
|
return getFormatter(formatType === 'number' ? 'num' : formatType, formatType === 'number' ? pattern : '')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleTextOverflow = (text, maxWidth) => {
|
||||||
|
if (!text || text.length <= 3) return text
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
let textWidth = ctx.measureText(text).width;
|
||||||
|
if (textWidth > maxWidth) {
|
||||||
|
let size = text.length - 3;
|
||||||
|
let newText = text.substr(0, size) + '...'
|
||||||
|
while (textWidth > maxWidth) {
|
||||||
|
size--
|
||||||
|
newText = text.substr(0, size) + '...'
|
||||||
|
textWidth = ctx.measureText(newText).width;
|
||||||
|
}
|
||||||
|
return text.substr(0, size - 5) + '...'
|
||||||
|
} else {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
|
||||||
|
const { config = {}, data = [] } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ height: '100%'}}>
|
||||||
|
{ data.length === 0 || data.some((item) => !Number.isFinite(item.value)) ? (
|
||||||
|
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE}/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
config?.sourceArea?.name ? (
|
||||||
|
<Treemap {...props}/>
|
||||||
|
) : (
|
||||||
|
<Heatmap {...props}/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,213 @@
|
||||||
|
import { Heatmap } from "@ant-design/charts";
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import * as uuid from 'uuid';
|
||||||
|
import { fixFormatter, generateColors, handleTextOverflow } from "./Chart";
|
||||||
|
|
||||||
|
function findMaxSize(a, b, n) {
|
||||||
|
let c = Math.min(a, b);
|
||||||
|
let rows, cols;
|
||||||
|
|
||||||
|
while (c >= 50) {
|
||||||
|
cols = Math.floor(a / c);
|
||||||
|
rows = Math.floor(b / c);
|
||||||
|
if (cols * rows >= n) {
|
||||||
|
return { rows: rows, cols: cols, itemWidth: c };
|
||||||
|
}
|
||||||
|
c-=0.01;
|
||||||
|
}
|
||||||
|
cols = Math.floor(a / 50)
|
||||||
|
rows = Math.ceil(n / cols)
|
||||||
|
return { rows, cols, itemWidth: c };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
const { config = {}, data = [] } = props
|
||||||
|
const {
|
||||||
|
top,
|
||||||
|
colors = [],
|
||||||
|
sourceColor = {},
|
||||||
|
} = config;
|
||||||
|
|
||||||
|
const containerRef = useRef()
|
||||||
|
const [size, setSize] = useState()
|
||||||
|
|
||||||
|
const color = useMemo(() => {
|
||||||
|
if (colors.length === 0 || !sourceColor?.key || data.length === 0) return undefined
|
||||||
|
const newColors = generateColors(colors, data).reverse()
|
||||||
|
if (newColors.length === 0) return undefined
|
||||||
|
return (a, b, c) => {
|
||||||
|
if (Number.isFinite(a?.value)) {
|
||||||
|
const splits = `${a.value}`.split('2024')
|
||||||
|
const value = Number(splits[1])
|
||||||
|
const index = Number(splits[0])
|
||||||
|
return newColors[index] || (value == 0 ? newColors[newColors.length - 1] : newColors[0])
|
||||||
|
} else {
|
||||||
|
return '#fff'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [data, colors, sourceColor])
|
||||||
|
|
||||||
|
const formatData = useMemo(() => {
|
||||||
|
if (!data || data.length === 0 || !size?.cols) return [];
|
||||||
|
const cols = size?.cols
|
||||||
|
const newData = []
|
||||||
|
let rowData = []
|
||||||
|
data.forEach((item, index) => {
|
||||||
|
if (rowData.length === cols) {
|
||||||
|
newData.unshift(cloneDeep(rowData))
|
||||||
|
rowData = []
|
||||||
|
}
|
||||||
|
rowData.push({
|
||||||
|
item,
|
||||||
|
name: item.name,
|
||||||
|
col: `${index % cols}`,
|
||||||
|
row: `${Math.floor(index / cols)}`,
|
||||||
|
value: Number(`${index}2024${item.valueColor}`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (rowData.length !== cols) {
|
||||||
|
const size = cols - rowData.length
|
||||||
|
for (let i=data.length; i<(data.length + size); i++) {
|
||||||
|
rowData.push({
|
||||||
|
col: `${i % cols}`,
|
||||||
|
row: `${Math.floor(i / cols)}`,
|
||||||
|
value: null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newData.unshift(rowData)
|
||||||
|
return [].concat.apply([], newData)
|
||||||
|
}, [JSON.stringify(data), size?.cols])
|
||||||
|
|
||||||
|
const handleResize = (size) => {
|
||||||
|
if (containerRef.current) {
|
||||||
|
const { offsetWidth, offsetHeight } = containerRef.current
|
||||||
|
if (size === 1) {
|
||||||
|
setSize({
|
||||||
|
rows: 1,
|
||||||
|
cols: 1,
|
||||||
|
itemWidth: offsetWidth,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let { cols, rows, itemWidth } = findMaxSize(offsetWidth, offsetHeight, size)
|
||||||
|
if (cols * itemWidth < offsetWidth) {
|
||||||
|
cols++
|
||||||
|
itemWidth = offsetWidth / cols
|
||||||
|
rows = Math.ceil(size / cols)
|
||||||
|
}
|
||||||
|
|
||||||
|
setSize({
|
||||||
|
rows,
|
||||||
|
cols,
|
||||||
|
itemWidth,
|
||||||
|
width: cols * itemWidth,
|
||||||
|
height: rows * itemWidth
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleResize(data.length)
|
||||||
|
const onResize = () => {
|
||||||
|
handleResize(data.length)
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', onResize)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', onResize)
|
||||||
|
}
|
||||||
|
}, [JSON.stringify(data)])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={containerRef} style={{ width: '100%', height: '100%' }}>
|
||||||
|
<div style={{ width: size?.width, height: size?.height }}>
|
||||||
|
<Heatmap {...{
|
||||||
|
animation: false,
|
||||||
|
data: formatData,
|
||||||
|
xField: 'col',
|
||||||
|
yField: 'row',
|
||||||
|
colorField: 'value',
|
||||||
|
shape: '',
|
||||||
|
color,
|
||||||
|
label: {
|
||||||
|
style: {
|
||||||
|
fill: '#fff',
|
||||||
|
shadowBlur: 2,
|
||||||
|
shadowColor: 'rgba(0, 0, 0, .45)',
|
||||||
|
},
|
||||||
|
formatter: (a, b, c) => {
|
||||||
|
return handleTextOverflow(a.name, size?.itemWidth)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
heatmapStyle: {
|
||||||
|
lineWidth: 0
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
tickLine: null,
|
||||||
|
label: null,
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
tickLine: null,
|
||||||
|
label: null,
|
||||||
|
},
|
||||||
|
legend: false,
|
||||||
|
tooltip: {
|
||||||
|
customContent: (title, items) => {
|
||||||
|
if (!items[0]) return;
|
||||||
|
const { color, data } = items[0];
|
||||||
|
const { item } = data;
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
const { format: formatColor, pattern: patternColor, unit: unitColor } = sourceColor || {}
|
||||||
|
const { name, value, nameColor, valueColor, displayName } = item || {}
|
||||||
|
const formatterColor = fixFormatter(formatColor, patternColor)
|
||||||
|
const markers = []
|
||||||
|
markers.push({
|
||||||
|
name: nameColor,
|
||||||
|
value: formatterColor ? formatterColor(valueColor) : valueColor,
|
||||||
|
unit: unitColor,
|
||||||
|
marker: <span style={{ position: 'absolute', left: 0, top: 0, display: 'block', borderRadius: '2px', backgroundColor: color, width: 12, height: 12 }}></span>
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 4 }}>
|
||||||
|
{
|
||||||
|
<h5 style={{ marginTop: 12, marginBottom: 12 }}>
|
||||||
|
{displayName}
|
||||||
|
</h5>
|
||||||
|
}
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
markers.map((item, index) => (
|
||||||
|
<div
|
||||||
|
style={{ display: 'block', paddingLeft: 18, marginBottom: 12, position: 'relative' }}
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
{item.marker}
|
||||||
|
<span
|
||||||
|
style={{ display: 'inline-flex', flex: 1, justifyContent: 'space-between' }}
|
||||||
|
>
|
||||||
|
<span style={{ marginRight: 16 }}>{item.name}:</span>
|
||||||
|
<span className="g2-tooltip-list-item-value">
|
||||||
|
{item.unit ? `${item.value}${item.unit}` : item.value}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
<div style={{ float: 'right'}}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ import { Treemap } from "@ant-design/charts";
|
||||||
import { Table } from "antd";
|
import { Table } from "antd";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { formatMessage } from "umi/locale";
|
import { formatMessage } from "umi/locale";
|
||||||
import { fixFormatter } from "./Treemap";
|
import { fixFormatter } from "./Chart";
|
||||||
|
|
||||||
export default (props) => {
|
export default (props) => {
|
||||||
|
|
||||||
|
@ -23,8 +23,8 @@ export default (props) => {
|
||||||
key: 'displayName',
|
key: 'displayName',
|
||||||
}];
|
}];
|
||||||
if (sourceArea) {
|
if (sourceArea) {
|
||||||
const { format: formatArea, unit: unitArea } = sourceArea || {}
|
const { format: formatArea, pattern: patternArea, unit: unitArea } = sourceArea || {}
|
||||||
const formatterArea = fixFormatter(formatArea)
|
const formatterArea = fixFormatter(formatArea, patternArea)
|
||||||
newColumns.push({
|
newColumns.push({
|
||||||
title: unitArea ? `${sourceArea.name}(${unitArea})` : sourceArea.name,
|
title: unitArea ? `${sourceArea.name}(${unitArea})` : sourceArea.name,
|
||||||
dataIndex: 'value',
|
dataIndex: 'value',
|
||||||
|
@ -35,8 +35,8 @@ export default (props) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (sourceColor) {
|
if (sourceColor) {
|
||||||
const { format: formatColor, unit: unitColor } = sourceColor
|
const { format: formatColor, pattern: patternColor, unit: unitColor } = sourceColor
|
||||||
const formatterColor = fixFormatter(formatColor)
|
const formatterColor = fixFormatter(formatColor, patternColor)
|
||||||
newColumns.push({
|
newColumns.push({
|
||||||
title: unitColor ? `${sourceColor.name}(${unitColor})` : sourceColor.name,
|
title: unitColor ? `${sourceColor.name}(${unitColor})` : sourceColor.name,
|
||||||
dataIndex: 'valueColor',
|
dataIndex: 'valueColor',
|
||||||
|
|
|
@ -1,61 +1,7 @@
|
||||||
import { getFormatter } from "@/utils/format";
|
import { getFormatter } from "@/utils/format";
|
||||||
import { Treemap } from "@ant-design/charts";
|
import { Treemap } from "@ant-design/charts";
|
||||||
import { Empty } from "antd";
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { fixFormatter, generateColors } from "./Chart";
|
||||||
const generateGradientColors = (startColor, endColor, steps) => {
|
|
||||||
function colorToRgb(color) {
|
|
||||||
const rgb = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(color);
|
|
||||||
return rgb ? {
|
|
||||||
r: parseInt(rgb[1], 16),
|
|
||||||
g: parseInt(rgb[2], 16),
|
|
||||||
b: parseInt(rgb[3], 16)
|
|
||||||
} : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function rgbToHex(r, g, b) {
|
|
||||||
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
const startRGB = colorToRgb(startColor);
|
|
||||||
const endRGB = colorToRgb(endColor);
|
|
||||||
const diffR = endRGB.r - startRGB.r;
|
|
||||||
const diffG = endRGB.g - startRGB.g;
|
|
||||||
const diffB = endRGB.b - startRGB.b;
|
|
||||||
|
|
||||||
const colors = [];
|
|
||||||
for (let i = 0; i <= steps; i++) {
|
|
||||||
const r = startRGB.r + (diffR * i / steps);
|
|
||||||
const g = startRGB.g + (diffG * i / steps);
|
|
||||||
const b = startRGB.b + (diffB * i / steps);
|
|
||||||
colors.push(rgbToHex(Math.round(r), Math.round(g), Math.round(b)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return colors;
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateColors = (colors, data) => {
|
|
||||||
if (!colors || colors.length <= 1 || !data || data.length <= 1 || data.length <= colors.length) return colors
|
|
||||||
const gradientSize = data.length - colors.length
|
|
||||||
const steps = Math.floor(gradientSize / (colors.length - 1)) + 1
|
|
||||||
let remainder = gradientSize % (colors.length - 1)
|
|
||||||
const newColors = []
|
|
||||||
for(let i=0; i<colors.length - 1; i++) {
|
|
||||||
let fixSteps = steps;
|
|
||||||
if (remainder > 0) {
|
|
||||||
fixSteps++
|
|
||||||
remainder--
|
|
||||||
}
|
|
||||||
const gradientColors = generateGradientColors(colors[i], colors[i+1], fixSteps)
|
|
||||||
newColors.push(...gradientColors.slice(0, gradientColors.length - 1))
|
|
||||||
}
|
|
||||||
newColors.push(colors[colors.length - 1])
|
|
||||||
return newColors
|
|
||||||
}
|
|
||||||
|
|
||||||
export const fixFormatter = (formatType) => {
|
|
||||||
return getFormatter(formatType === 'number' ? 'num' : formatType, formatType === 'number' ? '0,0.[00]a' : '')
|
|
||||||
}
|
|
||||||
|
|
||||||
export default (props) => {
|
export default (props) => {
|
||||||
|
|
||||||
|
@ -69,12 +15,12 @@ export default (props) => {
|
||||||
|
|
||||||
const color = useMemo(() => {
|
const color = useMemo(() => {
|
||||||
if (colors.length === 0 || !sourceColor?.key || data.length === 0) return undefined
|
if (colors.length === 0 || !sourceColor?.key || data.length === 0) return undefined
|
||||||
const newColors = generateColors(colors, data)
|
const newColors = generateColors(colors, data).reverse()
|
||||||
const sortData = data.sort((a, b) => a.valueColor - b.valueColor)
|
if (newColors.length === 0) return undefined
|
||||||
return ({ name }) => {
|
return ({ name }) => {
|
||||||
const index = sortData.findIndex((item) => item.name === name)
|
const index = data.findIndex((item) => item.name === name)
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
return newColors[index] || newColors[0]
|
return newColors[index] || (value == 0 ? newColors[newColors.length - 1] : newColors[0])
|
||||||
} else {
|
} else {
|
||||||
return newColors[0]
|
return newColors[0]
|
||||||
}
|
}
|
||||||
|
@ -83,12 +29,8 @@ export default (props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ height: '100%' }}>
|
<div style={{ height: '100%' }}>
|
||||||
{ data.length === 0 || data.some((item) => !Number.isFinite(item.value)) ? (
|
<Treemap {...{
|
||||||
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
animation: false,
|
||||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE}/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Treemap {...{
|
|
||||||
data: {
|
data: {
|
||||||
name: 'root',
|
name: 'root',
|
||||||
children: data
|
children: data
|
||||||
|
@ -96,17 +38,17 @@ export default (props) => {
|
||||||
autoFit: true,
|
autoFit: true,
|
||||||
color,
|
color,
|
||||||
colorField: 'name',
|
colorField: 'name',
|
||||||
legend: {
|
|
||||||
position: 'top-left',
|
|
||||||
itemName: {
|
|
||||||
formatter: (text) => {
|
|
||||||
const item = data.find((item) => item.name === text)
|
|
||||||
return item?.groupName || text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
label: {
|
label: {
|
||||||
formatter: (item) => item.displayName
|
formatter: (item, a, b, c) => item.displayName
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
position: 'top-left',
|
||||||
|
itemName: {
|
||||||
|
formatter: (text) => {
|
||||||
|
const item = data.find((item) => item.name === text)
|
||||||
|
return item?.groupName || text
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
customContent: (title, items) => {
|
customContent: (title, items) => {
|
||||||
|
@ -117,8 +59,8 @@ export default (props) => {
|
||||||
const markers = []
|
const markers = []
|
||||||
|
|
||||||
if (metricArea && tooltipArea !== false) {
|
if (metricArea && tooltipArea !== false) {
|
||||||
const { format: formatArea, unit: unitArea } = sourceArea || {}
|
const { format: formatArea, pattern: patternArea, unit: unitArea } = sourceArea || {}
|
||||||
const formatterArea = fixFormatter(formatArea)
|
const formatterArea = fixFormatter(formatArea, patternArea)
|
||||||
markers.push({
|
markers.push({
|
||||||
name: nameArea,
|
name: nameArea,
|
||||||
value: formatterArea ? formatterArea(value) : value,
|
value: formatterArea ? formatterArea(value) : value,
|
||||||
|
@ -128,8 +70,8 @@ export default (props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (metricColor) {
|
if (metricColor) {
|
||||||
const { format: formatColor, unit: unitColor } = sourceColor || {}
|
const { format: formatColor, pattern: patternColor, unit: unitColor } = sourceColor || {}
|
||||||
const formatterColor = fixFormatter(formatColor)
|
const formatterColor = fixFormatter(formatColor, patternColor)
|
||||||
markers.push({
|
markers.push({
|
||||||
name: nameColor,
|
name: nameColor,
|
||||||
value: formatterColor ? formatterColor(valueColor) : valueColor,
|
value: formatterColor ? formatterColor(valueColor) : valueColor,
|
||||||
|
@ -170,7 +112,6 @@ export default (props) => {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}} />
|
}} />
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -6,63 +6,146 @@ import { formatMessage } from "umi/locale";
|
||||||
import ConvertSvg from "@/components/Icons/Convert"
|
import ConvertSvg from "@/components/Icons/Convert"
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import ColorPicker from "./ColorPicker";
|
import ColorPicker from "./ColorPicker";
|
||||||
import Treemap from "./Treemap";
|
import Chart from "./Chart";
|
||||||
import Table from "./Table";
|
import Table from "./Table";
|
||||||
import GradientColorPicker from "./GradientColorPicker";
|
import GradientColorPicker from "./GradientColorPicker";
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
import request from "@/utils/request";
|
import request from "@/utils/request";
|
||||||
import { formatTimeRange } from "@/lib/elasticsearch/util";
|
import { formatTimeRange } from "@/lib/elasticsearch/util";
|
||||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||||
import * as uuid from 'uuid';
|
import { getRollupEnabled } from "@/utils/authority";
|
||||||
|
import { getStatistics, ROLLUP_FIELDS } from "@/components/vendor/index_pattern_management/public/components/field_editor/field_editor";
|
||||||
|
|
||||||
|
const DEFAULT_TOP = 15;
|
||||||
|
const DEFAULT_COLORS = ['#00bb1b', '#fcca00', '#ff4d4f']
|
||||||
|
|
||||||
|
function generate20BitUUID() {
|
||||||
|
let characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
let uuid = characters[Math.floor(Math.random() * characters.length)];
|
||||||
|
const buffer = new Uint8Array(9);
|
||||||
|
crypto.getRandomValues(buffer);
|
||||||
|
for (let i = 0; i < buffer.length; i++) {
|
||||||
|
uuid += buffer[i].toString(16).padStart(2, '0');
|
||||||
|
}
|
||||||
|
return uuid.slice(0, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatComplexStatistic = (statistic) => {
|
||||||
|
if (statistic?.includes('rate')) {
|
||||||
|
return 'rate'
|
||||||
|
} else if (statistic?.includes('latency')) {
|
||||||
|
return 'latency'
|
||||||
|
}
|
||||||
|
return 'max'
|
||||||
|
}
|
||||||
|
|
||||||
export default (props) => {
|
export default (props) => {
|
||||||
|
|
||||||
const { type, clusterID, timeRange } = props;
|
const { type, clusterID, timeRange, isAgent } = props;
|
||||||
|
|
||||||
const [currentMode, setCurrentMode] = useState('treemap')
|
const [currentMode, setCurrentMode] = useState('treemap')
|
||||||
|
|
||||||
const [metrics, setMetrics] = useState([])
|
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
top: 15,
|
top: DEFAULT_TOP,
|
||||||
colors: ['#00bb1b', '#fcca00', '#ff4d4f']
|
colors: DEFAULT_COLORS
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isRollupEnabled = getRollupEnabled() === 'true'
|
||||||
|
|
||||||
const [config, setConfig] = useState({})
|
const [config, setConfig] = useState({})
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [data, setData] = useState([])
|
const [data, setData] = useState([])
|
||||||
const [result, setResult] = useState()
|
const [result, setResult] = useState()
|
||||||
|
const [selectedView, setSelectedView] = useState()
|
||||||
const searchParamsRef = useRef()
|
const searchParamsRef = useRef()
|
||||||
|
|
||||||
const fetchMetrics = async (type) => {
|
const fetchFields = async (clusterID, viewID, type, isAgent) => {
|
||||||
|
if (!clusterID || !viewID) return;
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const res = await request(`/collection/metric/_search`, {
|
const res = await request(`/elasticsearch/${clusterID}/saved_objects/_bulk_get`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: [{
|
||||||
size: 10000,
|
id: viewID,
|
||||||
from: 0,
|
type: "view"
|
||||||
query: { bool: { filter: [{ "term": { "level": type === 'index' ? 'indices' : type } }] }}
|
}]
|
||||||
}
|
|
||||||
})
|
})
|
||||||
if (res?.hits?.hits) {
|
if (res && !res.error && Array.isArray(res.saved_objects) && res.saved_objects[0]) {
|
||||||
const newMetrics = res?.hits?.hits.filter((item) => {
|
const newView = res.saved_objects[0]
|
||||||
const { items = [] } = item._source;
|
let { fieldFormatMap, fields } = newView.attributes || {}
|
||||||
if (items.length === 0) return false
|
let { complex_fields: complexFields } = newView
|
||||||
return true;
|
try {
|
||||||
}).map((item) => ({ ...item._source }))
|
fieldFormatMap = JSON.parse(fieldFormatMap) || {}
|
||||||
setMetrics(newMetrics)
|
fields = JSON.parse(fields) || []
|
||||||
if (newMetrics.length > 0 && (!formData.sourceArea && !formData.sourceColor)) {
|
complexFields = JSON.parse(complexFields)
|
||||||
|
const keys = Object.keys(complexFields || {})
|
||||||
|
complexFields = keys.map((key) => {
|
||||||
|
const item = complexFields?.[key] || {}
|
||||||
|
return {
|
||||||
|
name: key,
|
||||||
|
metric_config: item
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
fieldFormatMap = {}
|
||||||
|
fields = []
|
||||||
|
}
|
||||||
|
if (!Array.isArray(fields)) fields = []
|
||||||
|
if (!Array.isArray(complexFields)) complexFields = []
|
||||||
|
if (!newView.attributes) newView.attributes = {}
|
||||||
|
newView.fieldFormatMap = fieldFormatMap
|
||||||
|
newView.fields = fields.filter((item) =>
|
||||||
|
!!item.metric_config &&
|
||||||
|
!!item.metric_config.name &&
|
||||||
|
item.metric_config.tags?.includes(type === 'index' ? 'indices' : type) &&
|
||||||
|
item.metric_config.tags?.includes(isAgent ? 'agent' : 'agentless')
|
||||||
|
).map((item) => {
|
||||||
|
if(!item.metric_config.option_aggs || item.metric_config.option_aggs.length === 0) {
|
||||||
|
item.metric_config.option_aggs = isRollupEnabled ? ROLLUP_FIELDS[item.name]: getStatistics(item.type)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
format: fieldFormatMap[item.name]?.id || 'number',
|
||||||
|
pattern: fieldFormatMap[item.name]?.params?.pattern,
|
||||||
|
}
|
||||||
|
}).concat(complexFields.filter((item) =>
|
||||||
|
!!item.metric_config &&
|
||||||
|
!!item.metric_config.name &&
|
||||||
|
!!item.metric_config.function &&
|
||||||
|
item.metric_config.tags?.includes(type === 'index' ? 'indices' : type) &&
|
||||||
|
item.metric_config.tags?.includes(isAgent ? 'agent' : 'agentless')
|
||||||
|
).map((item) => {
|
||||||
|
item.metric_config.option_aggs = [formatComplexStatistic(Object.keys(item.metric_config.function || {})[0])]
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
format: fieldFormatMap[item.name]?.id || 'number',
|
||||||
|
pattern: fieldFormatMap[item.name]?.params?.pattern,
|
||||||
|
isComplex: true,
|
||||||
|
}
|
||||||
|
})).filter((item) =>
|
||||||
|
item.metric_config.option_aggs &&
|
||||||
|
item.metric_config.option_aggs.length > 0
|
||||||
|
)
|
||||||
|
|
||||||
|
setSelectedView(newView)
|
||||||
|
if (newView.fields.length > 0 && (!formData.sourceArea && !formData.sourceColor)) {
|
||||||
|
const initField = newView.fields[0]
|
||||||
|
const initStatistic = newView.fields[0].metric_config?.option_aggs?.[0] || 'max'
|
||||||
const newFormData = {
|
const newFormData = {
|
||||||
...cloneDeep(formData),
|
...cloneDeep(formData),
|
||||||
sourceArea: newMetrics[0],
|
sourceArea: initField,
|
||||||
statisticArea: newMetrics[0]?.items[0]?.statistic,
|
statisticArea: initStatistic,
|
||||||
sourceColor: newMetrics[0],
|
sourceColor: initField,
|
||||||
statisticColor: newMetrics[0].items[0]?.statistic
|
statisticColor: initStatistic
|
||||||
}
|
}
|
||||||
setFormData(newFormData)
|
setFormData(newFormData)
|
||||||
fetchData(type, clusterID, timeRange, newFormData)
|
fetchData(type, clusterID, timeRange, newFormData)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
setFormData({
|
||||||
|
top: DEFAULT_TOP,
|
||||||
|
colors: DEFAULT_COLORS
|
||||||
|
})
|
||||||
}
|
}
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
@ -72,20 +155,94 @@ export default (props) => {
|
||||||
if (shouldLoading) {
|
if (shouldLoading) {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
}
|
}
|
||||||
const { top, sourceArea = {}, statisticArea, statisticColor, sourceColor = {} } = formData
|
const { top, sourceArea, statisticArea, statisticColor, sourceColor } = formData
|
||||||
const newTimeRange = formatTimeRange(timeRange);
|
const newTimeRange = formatTimeRange(timeRange);
|
||||||
searchParamsRef.current = { type, clusterID, formData }
|
searchParamsRef.current = { type, clusterID, formData }
|
||||||
const sortKey = sourceArea?.items?.[0]?.name || sourceColor?.items?.[0]?.name
|
let areaValueID
|
||||||
|
let colorValueID
|
||||||
|
let areaFormula
|
||||||
|
let colorFormula
|
||||||
|
const items = []
|
||||||
|
const formulas = []
|
||||||
|
let isAreaRate = false
|
||||||
|
let isColorRate = false
|
||||||
|
let isAreaLatency = false
|
||||||
|
let isColorLatency = false
|
||||||
|
if (sourceArea) {
|
||||||
|
areaValueID = generate20BitUUID()
|
||||||
|
if (sourceArea.isComplex) {
|
||||||
|
if (sourceArea.metric_config.function) {
|
||||||
|
const func = Object.keys(sourceArea.metric_config.function || {})?.[0]
|
||||||
|
if (func?.includes('rate')) {
|
||||||
|
isAreaRate = true
|
||||||
|
}
|
||||||
|
if (func?.includes('latency')) {
|
||||||
|
isAreaLatency = true
|
||||||
|
}
|
||||||
|
items.push({
|
||||||
|
name: areaValueID,
|
||||||
|
function: sourceArea.metric_config.function,
|
||||||
|
})
|
||||||
|
areaFormula = isAreaRate ? `${areaValueID}/{{.bucket_size_in_second}}` : areaValueID
|
||||||
|
formulas.push(areaFormula)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (statisticArea) {
|
||||||
|
items.push({
|
||||||
|
function: {
|
||||||
|
[statisticArea]: {
|
||||||
|
field: sourceArea.name,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
name: areaValueID,
|
||||||
|
})
|
||||||
|
areaFormula = areaValueID
|
||||||
|
formulas.push(areaFormula)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sourceColor) {
|
||||||
|
colorValueID = generate20BitUUID()
|
||||||
|
if (sourceColor.isComplex) {
|
||||||
|
if (sourceColor.metric_config.function) {
|
||||||
|
const func = Object.keys(sourceColor.metric_config.function || {})?.[0]
|
||||||
|
if (func?.includes('rate')) {
|
||||||
|
isColorRate = true
|
||||||
|
}
|
||||||
|
if (func?.includes('latency')) {
|
||||||
|
isColorLatency = true
|
||||||
|
}
|
||||||
|
items.push({
|
||||||
|
name: colorValueID,
|
||||||
|
function: sourceColor.metric_config.function,
|
||||||
|
})
|
||||||
|
colorFormula = isColorRate ? `${colorValueID}/{{.bucket_size_in_second}}` : colorValueID
|
||||||
|
formulas.push(colorFormula)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (statisticColor) {
|
||||||
|
items.push({
|
||||||
|
function: {
|
||||||
|
[statisticColor]: {
|
||||||
|
field: sourceColor.name,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
name: colorValueID,
|
||||||
|
})
|
||||||
|
colorFormula = colorValueID
|
||||||
|
formulas.push(colorFormula)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const sortKey = areaValueID || colorValueID
|
||||||
const body = {
|
const body = {
|
||||||
"index_pattern": ".infini_metrics*",
|
"index_pattern": ".infini_metrics*",
|
||||||
"time_field": "timestamp",
|
|
||||||
"bucket_size": "auto",
|
|
||||||
"filter": {
|
"filter": {
|
||||||
"bool": {
|
"bool": {
|
||||||
"must": [{
|
"must": [{
|
||||||
"term": {
|
"term": {
|
||||||
"metadata.name": {
|
"metadata.name": {
|
||||||
"value": `${type}_stats`
|
"value": isAgent && type === 'index' ? `shard_stats` : `${type}_stats`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
|
@ -116,20 +273,8 @@ export default (props) => {
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"formulas": [sourceArea?.formula, sourceColor?.formula].filter((item) => !!item),
|
"formulas": formulas,
|
||||||
"items": [...(sourceArea?.items || [])?.map((item) => {
|
"items": items,
|
||||||
item.statistic = statisticArea
|
|
||||||
if (item.statistic === 'rate') {
|
|
||||||
item.statistic = 'derivative'
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
}),...(sourceColor?.items || [])?.map((item) => {
|
|
||||||
item.statistic = statisticColor
|
|
||||||
if (item.statistic === 'rate') {
|
|
||||||
item.statistic = 'derivative'
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
})].filter((item) => !!item),
|
|
||||||
"groups": [{
|
"groups": [{
|
||||||
"field": type === 'shard' ? `metadata.labels.shard_id` : `metadata.labels.${type}_name`,
|
"field": type === 'shard' ? `metadata.labels.shard_id` : `metadata.labels.${type}_name`,
|
||||||
"limit": top
|
"limit": top
|
||||||
|
@ -139,9 +284,9 @@ export default (props) => {
|
||||||
"key": sortKey
|
"key": sortKey
|
||||||
}] : undefined
|
}] : undefined
|
||||||
}
|
}
|
||||||
if (statisticArea !== 'rate' && statisticColor !== 'rate') {
|
if ((isAreaRate || isColorRate) || (isAreaLatency || isColorLatency)) {
|
||||||
delete body['time_field']
|
body['time_field'] = "timestamp"
|
||||||
delete body['bucket_size']
|
body['bucket_size'] = "auto"
|
||||||
}
|
}
|
||||||
const res = await request(`/elasticsearch/infini_default_system_cluster/visualization/data`, {
|
const res = await request(`/elasticsearch/infini_default_system_cluster/visualization/data`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
@ -149,7 +294,28 @@ export default (props) => {
|
||||||
})
|
})
|
||||||
if (res && !res.error) {
|
if (res && !res.error) {
|
||||||
setResult(res)
|
setResult(res)
|
||||||
setConfig(cloneDeep(formData))
|
const newConfig = cloneDeep(formData)
|
||||||
|
if (newConfig.sourceArea?.name && newConfig.sourceArea?.metric_config?.name) {
|
||||||
|
newConfig.sourceArea = {
|
||||||
|
key: newConfig.sourceArea?.name,
|
||||||
|
name: newConfig.sourceArea.metric_config.name,
|
||||||
|
formula: areaFormula,
|
||||||
|
format: newConfig.sourceArea.format,
|
||||||
|
pattern: newConfig.sourceArea.pattern,
|
||||||
|
unit: newConfig.sourceArea.metric_config.unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newConfig.sourceColor?.name && newConfig.sourceColor?.metric_config?.name) {
|
||||||
|
newConfig.sourceColor = {
|
||||||
|
key: newConfig.sourceColor?.name,
|
||||||
|
name: newConfig.sourceColor.metric_config.name,
|
||||||
|
formula: colorFormula,
|
||||||
|
format: newConfig.sourceColor.format,
|
||||||
|
pattern: newConfig.sourceColor.pattern,
|
||||||
|
unit: newConfig.sourceColor.metric_config.unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setConfig(newConfig)
|
||||||
} else {
|
} else {
|
||||||
setResult()
|
setResult()
|
||||||
}
|
}
|
||||||
|
@ -179,7 +345,10 @@ export default (props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchMetrics(type)
|
fetchFields(clusterID, 'infini_metrics', type, isAgent)
|
||||||
|
}, [clusterID, type, isAgent])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
}, [type])
|
}, [type])
|
||||||
|
|
||||||
const isTreemap = useMemo(() => {
|
const isTreemap = useMemo(() => {
|
||||||
|
@ -214,7 +383,7 @@ export default (props) => {
|
||||||
sortKey = 'value'
|
sortKey = 'value'
|
||||||
} else {
|
} else {
|
||||||
if (sourceColor) {
|
if (sourceColor) {
|
||||||
const key = uuid.v4();
|
const key = generate20BitUUID();
|
||||||
object['metricArea'] = `metric_${key}`
|
object['metricArea'] = `metric_${key}`
|
||||||
object['value'] = 1
|
object['value'] = 1
|
||||||
object['nameArea'] = `name_${key}`
|
object['nameArea'] = `name_${key}`
|
||||||
|
@ -270,12 +439,9 @@ export default (props) => {
|
||||||
/>
|
/>
|
||||||
</Radio.Button>
|
</Radio.Button>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
<Input
|
<div className={styles.label}>
|
||||||
style={{ width: "60px", marginBottom: 12 }}
|
Top
|
||||||
className={styles.borderRadiusLeft}
|
</div>
|
||||||
disabled
|
|
||||||
defaultValue={"Top"}
|
|
||||||
/>
|
|
||||||
<InputNumber
|
<InputNumber
|
||||||
style={{ width: "80px", marginBottom: 12, marginRight: 12 }}
|
style={{ width: "80px", marginBottom: 12, marginRight: 12 }}
|
||||||
className={styles.borderRadiusRight}
|
className={styles.borderRadiusRight}
|
||||||
|
@ -285,22 +451,25 @@ export default (props) => {
|
||||||
precision={0}
|
precision={0}
|
||||||
onChange={(value) => onFormDataChange({ top: value })}
|
onChange={(value) => onFormDataChange({ top: value })}
|
||||||
/>
|
/>
|
||||||
<Input
|
<div className={styles.label}>
|
||||||
style={{ width: "80px", marginBottom: 12 }}
|
{formatMessage({ id: "cluster.monitor.topn.area" })}
|
||||||
className={styles.borderRadiusLeft}
|
</div>
|
||||||
disabled
|
|
||||||
defaultValue={"面积指标"}
|
|
||||||
/>
|
|
||||||
<Select
|
<Select
|
||||||
style={{ width: "150px", marginBottom: 12 }}
|
style={{ width: "150px", marginBottom: 12 }}
|
||||||
value={formData.sourceArea?.key}
|
value={formData.sourceArea?.name}
|
||||||
dropdownMatchSelectWidth={false}
|
dropdownMatchSelectWidth={false}
|
||||||
onChange={(value, option) => {
|
onChange={(value, option) => {
|
||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
const { items = [] } = option?.props?.metric || {}
|
const { isComplex } = option?.props?.metric || {}
|
||||||
|
let statisticArea;
|
||||||
|
if (isComplex) {
|
||||||
|
statisticArea = formatComplexStatistic(Object.keys(option?.props?.metric?.metric_config?.function || {})[0])
|
||||||
|
} else {
|
||||||
|
const { items = [] } = option?.props?.metric || {}
|
||||||
|
statisticArea = items[0]?.statistic === 'derivative' ? 'rate' : (items[0]?.statistic || 'max')
|
||||||
|
}
|
||||||
onFormDataChange({
|
onFormDataChange({
|
||||||
statisticArea: items[0]?.statistic === 'derivative' ? 'rate' : items[0]?.statistic,
|
statisticArea: statisticArea,
|
||||||
sourceArea: option?.props?.metric
|
sourceArea: option?.props?.metric
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
@ -313,9 +482,9 @@ export default (props) => {
|
||||||
allowClear
|
allowClear
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
metrics.map((item) => (
|
(selectedView?.fields || []).filter((item) => !!item.metric_config).map((item) => (
|
||||||
<Select.Option key={item.key} metric={item}>
|
<Select.Option key={item.name} metric={item}>
|
||||||
{item.name}
|
{item.metric_config.name}
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -328,30 +497,36 @@ export default (props) => {
|
||||||
onChange={(value) => onFormDataChange({ statisticArea: value })}
|
onChange={(value) => onFormDataChange({ statisticArea: value })}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
formData.sourceArea?.statistics?.filter((item) => !!item).map((item) => (
|
formData.sourceArea?.metric_config?.option_aggs?.filter((item) => !!item).map((item) => {
|
||||||
<Select.Option key={item}>
|
const limits = ROLLUP_FIELDS[formData.sourceArea.name]
|
||||||
{item.toUpperCase()}
|
return (
|
||||||
</Select.Option>
|
<Select.Option key={item} disabled={limits && isRollupEnabled ? !limits.includes(item) : false}>
|
||||||
))
|
{item.toUpperCase()}
|
||||||
|
</Select.Option>
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</Select>
|
</Select>
|
||||||
<Button style={{ width: 32, marginBottom: 12, padding: 0, marginRight: 6, borderRadius: 4 }} onClick={() => onMetricExchange()}><Icon style={{ fontSize: 16 }} component={ConvertSvg}/></Button>
|
<Button style={{ width: 32, marginBottom: 12, padding: 0, marginRight: 6, borderRadius: 4 }} onClick={() => onMetricExchange()}><Icon style={{ fontSize: 16 }} component={ConvertSvg}/></Button>
|
||||||
<Input
|
<div className={styles.label}>
|
||||||
style={{ width: "80px", marginBottom: 12 }}
|
{formatMessage({ id: "cluster.monitor.topn.color" })}
|
||||||
className={styles.borderRadiusLeft}
|
</div>
|
||||||
disabled
|
|
||||||
defaultValue={"颜色指标"}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
style={{ width: "150px", marginBottom: 12 }}
|
style={{ width: "150px", marginBottom: 12 }}
|
||||||
value={formData.sourceColor?.key}
|
value={formData.sourceColor?.name}
|
||||||
dropdownMatchSelectWidth={false}
|
dropdownMatchSelectWidth={false}
|
||||||
onChange={(value, option) => {
|
onChange={(value, option) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
const { items = [] } = option?.props?.metric || {}
|
const { isComplex } = option?.props?.metric || {}
|
||||||
|
let statisticColor;
|
||||||
|
if (isComplex) {
|
||||||
|
statisticColor = formatComplexStatistic(Object.keys(option?.props?.metric?.metric_config?.function || {})[0])
|
||||||
|
} else {
|
||||||
|
const { items = [] } = option?.props?.metric || {}
|
||||||
|
statisticColor = items[0]?.statistic === 'derivative' ? 'rate' : (items[0]?.statistic || 'max')
|
||||||
|
}
|
||||||
onFormDataChange({
|
onFormDataChange({
|
||||||
statisticColor: items[0]?.statistic === 'derivative' ? 'rate' : items[0]?.statistic,
|
statisticColor: statisticColor,
|
||||||
sourceColor: option?.props?.metric
|
sourceColor: option?.props?.metric
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
@ -365,9 +540,9 @@ export default (props) => {
|
||||||
allowClear
|
allowClear
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
metrics.map((item) => (
|
(selectedView?.fields || []).filter((item) => !!item.metric_config).map((item) => (
|
||||||
<Select.Option key={item.key} metric={item}>
|
<Select.Option key={item.name} metric={item}>
|
||||||
{item.name}
|
{item.metric_config.name}
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -379,18 +554,19 @@ export default (props) => {
|
||||||
onChange={(value) => onFormDataChange({ statisticColor: value })}
|
onChange={(value) => onFormDataChange({ statisticColor: value })}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
formData.sourceColor?.statistics?.filter((item) => !!item).map((item) => (
|
formData.sourceColor?.metric_config?.option_aggs?.filter((item) => !!item).map((item) => {
|
||||||
<Select.Option key={item}>
|
const limits = ROLLUP_FIELDS[formData.sourceColor.name]
|
||||||
{item.toUpperCase()}
|
return (
|
||||||
</Select.Option>
|
<Select.Option key={item} disabled={limits && isRollupEnabled ? !limits.includes(item) : false}>
|
||||||
))
|
{item.toUpperCase()}
|
||||||
|
</Select.Option>
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</Select>
|
</Select>
|
||||||
<Input
|
<div className={styles.label} style={{ borderTopLeftRadius: 0, borderBottomLeftRadius: 0 }}>
|
||||||
style={{ width: "60px", marginBottom: 12 }}
|
{formatMessage({ id: "cluster.monitor.topn.theme" })}
|
||||||
disabled
|
</div>
|
||||||
defaultValue={"主题"}
|
|
||||||
/>
|
|
||||||
<GradientColorPicker className={styles.borderRadiusRight} style={{ marginRight: 12, marginBottom: 12 }} value={formData.colors || []} onChange={(value) => {
|
<GradientColorPicker className={styles.borderRadiusRight} style={{ marginRight: 12, marginBottom: 12 }} value={formData.colors || []} onChange={(value) => {
|
||||||
onFormDataChange({ colors: value })
|
onFormDataChange({ colors: value })
|
||||||
setConfig({
|
setConfig({
|
||||||
|
@ -398,7 +574,7 @@ export default (props) => {
|
||||||
colors: value
|
colors: value
|
||||||
})
|
})
|
||||||
}}/>
|
}}/>
|
||||||
<Button style={{ marginBottom: 12 }} className={styles.borderRadiusLeft} type="primary" onClick={() => fetchData(type, clusterID, timeRange, formData)}>{formatMessage({ id: "form.button.search" })}</Button>
|
<Button style={{ marginBottom: 12 }} className={styles.borderRadiusLeft} type="primary" onClick={() => fetchData(type, clusterID, timeRange, formData)}>{formatMessage({ id: "form.button.apply" })}</Button>
|
||||||
</Input.Group>
|
</Input.Group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -414,7 +590,7 @@ export default (props) => {
|
||||||
</CopyToClipboard>
|
</CopyToClipboard>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{ isTreemap ? <Treemap config={config} data={formatData} /> : <Table type={type} config={config} data={formatData}/> }
|
{ isTreemap ? <Chart config={config} data={formatData} /> : <Table type={type} config={config} data={formatData}/> }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Spin>
|
</Spin>
|
||||||
|
|
|
@ -21,6 +21,20 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
width: auto;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0px 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 32px;
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-top-right-radius: 0px !important;
|
||||||
|
border-bottom-right-radius: 0px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
|
|
@ -72,9 +72,14 @@ export function getAuthorizationHeader() {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getRollupEnabled() {
|
||||||
|
return localStorage.getItem("infini-rollup-enabled");
|
||||||
|
}
|
||||||
|
|
||||||
(async function() {
|
(async function() {
|
||||||
const authRes = await request("/setting/application");
|
const res = await request("/setting/application");
|
||||||
if (authRes && !authRes.error) {
|
if (res && !res.error) {
|
||||||
localStorage.setItem("infini-auth", authRes.auth_enabled);
|
localStorage.setItem("infini-auth", res.auth_enabled);
|
||||||
|
localStorage.setItem('infini-rollup-enabled', res.system_cluster?.rollup_enabled || false)
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -427,3 +427,14 @@ export const formatToUniversalTime = (time, format, timezone) => {
|
||||||
if (!time) return '-';
|
if (!time) return '-';
|
||||||
return moment(time).tz(timezone || getTimezone()).format(format || "YYYY-MM-DD HH:mm:ss (G[M]TZ)")
|
return moment(time).tz(timezone || getTimezone()).format(format || "YYYY-MM-DD HH:mm:ss (G[M]TZ)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const generate20BitUUID = () => {
|
||||||
|
let characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
let uuid = characters[Math.floor(Math.random() * characters.length)];
|
||||||
|
const buffer = new Uint8Array(9);
|
||||||
|
crypto.getRandomValues(buffer);
|
||||||
|
for (let i = 0; i < buffer.length; i++) {
|
||||||
|
uuid += buffer[i].toString(16).padStart(2, '0');
|
||||||
|
}
|
||||||
|
return uuid.slice(0, 20);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue