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
|
||||
widget={histogramState.widget}
|
||||
range={histogramState.range}
|
||||
queryParams={queryParams?.filters || {}}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
|
@ -13,6 +13,7 @@ import { getTimezone } from "@/utils/utils";
|
|||
import { getContext } from "@/pages/DataManagement/context";
|
||||
import { ESPrefix } from "@/services/common";
|
||||
import CollectStatus from "@/components/CollectStatus";
|
||||
import styles from "./index.less"
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
|
@ -140,7 +141,7 @@ const Monitor = (props) => {
|
|||
<div>
|
||||
<BreadcrumbList data={breadcrumbList} />
|
||||
|
||||
<Card bodyStyle={{ padding: 15 }}>
|
||||
<Card bodyStyle={{ padding: 16 }}>
|
||||
{
|
||||
selectedCluster?.id ? (
|
||||
<>
|
||||
|
@ -187,58 +188,59 @@ const Monitor = (props) => {
|
|||
<CollectStatus fetchUrl={`${ESPrefix}/${selectedCluster?.id}/_collection_stats`}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
activeKey={param?.tab || panes[0]?.key}
|
||||
onChange={(key) => {
|
||||
setParam({ ...param, tab: key });
|
||||
}}
|
||||
tabBarGutter={10}
|
||||
destroyInactiveTabPane
|
||||
animated={false}
|
||||
>
|
||||
{panes.map((pane) => (
|
||||
<TabPane tab={pane.title} key={pane.key}>
|
||||
<Spin spinning={spinning && !!state.refresh}>
|
||||
<StatisticBar
|
||||
setSpinning={setSpinning}
|
||||
onInfoChange={onInfoChange}
|
||||
{...state}
|
||||
{...extraParams}
|
||||
/>
|
||||
</Spin>
|
||||
<div style={{ marginTop: 15 }}>
|
||||
{checkPaneParams({
|
||||
...state,
|
||||
...extraParams,
|
||||
}) ? (
|
||||
typeof pane.component == "string" ? (
|
||||
pane.component
|
||||
) : (
|
||||
<pane.component
|
||||
selectedCluster={selectedCluster}
|
||||
isAgent={isAgent}
|
||||
{...state}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleTimeIntervalChange={(timeInterval) => {
|
||||
onTimeSettingsChange({
|
||||
timeInterval,
|
||||
})
|
||||
setState({
|
||||
...state,
|
||||
timeInterval,
|
||||
});
|
||||
}}
|
||||
setSpinning={setSpinning}
|
||||
{...extraParams}
|
||||
bucketSize={state.timeInterval}
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</TabPane>
|
||||
))}
|
||||
</Tabs>
|
||||
<div className={styles.tabs}>
|
||||
<Tabs
|
||||
activeKey={param?.tab || panes[0]?.key}
|
||||
onChange={(key) => {
|
||||
setParam({ ...param, tab: key });
|
||||
}}
|
||||
tabBarGutter={10}
|
||||
destroyInactiveTabPane
|
||||
animated={false}
|
||||
>
|
||||
{panes.map((pane) => (
|
||||
<TabPane tab={pane.title} key={pane.key}>
|
||||
<Spin spinning={spinning && !!state.refresh}>
|
||||
<StatisticBar
|
||||
setSpinning={setSpinning}
|
||||
onInfoChange={onInfoChange}
|
||||
{...state}
|
||||
{...extraParams}
|
||||
/>
|
||||
</Spin>
|
||||
<div style={{ marginTop: 15 }}>
|
||||
{checkPaneParams({
|
||||
...state,
|
||||
...extraParams,
|
||||
}) ? (
|
||||
typeof pane.component == "string" ? (
|
||||
pane.component
|
||||
) : (
|
||||
<pane.component
|
||||
selectedCluster={selectedCluster}
|
||||
isAgent={isAgent}
|
||||
{...state}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleTimeIntervalChange={(timeInterval) => {
|
||||
onTimeSettingsChange({
|
||||
timeInterval,
|
||||
})
|
||||
setState({
|
||||
...state,
|
||||
timeInterval,
|
||||
});
|
||||
}}
|
||||
setSpinning={setSpinning}
|
||||
{...extraParams}
|
||||
bucketSize={state.timeInterval}
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</TabPane>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
</>
|
||||
) : <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(
|
||||
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.type = type;
|
||||
|
@ -51,6 +51,7 @@ export class SimpleSavedObject<T = unknown> {
|
|||
this.references = references || [];
|
||||
this._version = version;
|
||||
this.migrationVersion = migrationVersion;
|
||||
this.attributes.complexFields = complex_fields
|
||||
if (error) {
|
||||
this.error = error;
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ export const fieldList = (
|
|||
}
|
||||
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) =>
|
||||
shortDotsEnable ? shortenDottedString(name) : name;
|
||||
constructor() {
|
||||
|
@ -71,7 +71,7 @@ export const fieldList = (
|
|||
...(this.groups.get(type) || new Map()).values(),
|
||||
];
|
||||
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.setByName(newField);
|
||||
this.setByGroup(newField);
|
||||
|
|
|
@ -134,6 +134,14 @@ export class IndexPatternField implements IFieldType {
|
|||
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() {
|
||||
return {
|
||||
count: this.count,
|
||||
|
@ -148,7 +156,8 @@ export class IndexPatternField implements IFieldType {
|
|||
searchable: this.searchable,
|
||||
aggregatable: this.aggregatable,
|
||||
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,
|
||||
subType: this.subType,
|
||||
format: getFormatterForField ? getFormatterForField(this).toJSON() : undefined,
|
||||
metric_config: this.metric_config,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -117,7 +117,14 @@ export class IndexPattern implements IIndexPattern {
|
|||
|
||||
// set values
|
||||
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;
|
||||
|
||||
|
@ -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() {
|
||||
const scriptFields: any = {};
|
||||
if (!this.fields) {
|
||||
|
@ -381,6 +413,20 @@ export class IndexPattern implements IIndexPattern {
|
|||
? undefined
|
||||
: 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 {
|
||||
title: this.title,
|
||||
viewName: this.viewName,
|
||||
|
@ -390,6 +436,7 @@ export class IndexPattern implements IIndexPattern {
|
|||
? JSON.stringify(this.sourceFilters)
|
||||
: undefined,
|
||||
fields: this.fields ? JSON.stringify(this.fields) : undefined,
|
||||
complex_fields: formatComplexFields ? JSON.stringify(formatComplexFields) : undefined,
|
||||
fieldFormatMap,
|
||||
type: this.type,
|
||||
typeMeta: this.typeMeta ? JSON.stringify(this.typeMeta) : undefined,
|
||||
|
|
|
@ -358,6 +358,7 @@ export class IndexPatternsService {
|
|||
sourceFilters,
|
||||
fieldFormatMap,
|
||||
typeMeta,
|
||||
complexFields,
|
||||
},
|
||||
type,
|
||||
} = savedObject;
|
||||
|
@ -370,6 +371,7 @@ export class IndexPatternsService {
|
|||
? JSON.parse(fieldFormatMap)
|
||||
: {};
|
||||
const parsedFields: FieldSpec[] = fields ? JSON.parse(fields) : [];
|
||||
const parsedComplexFields = complexFields ? JSON.parse(complexFields) : [];
|
||||
|
||||
this.addFormatsToFields(parsedFields, parsedFieldFormatMap);
|
||||
return {
|
||||
|
@ -383,6 +385,7 @@ export class IndexPatternsService {
|
|||
fields: this.fieldArrayToMap(parsedFields),
|
||||
typeMeta: parsedTypeMeta,
|
||||
type,
|
||||
complexFields: parsedComplexFields
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -409,7 +412,6 @@ export class IndexPatternsService {
|
|||
// if (!savedObject.version) {
|
||||
// throw new SavedObjectNotFound(savedObjectType, id, 'management/kibana/indexPatterns');
|
||||
// }
|
||||
|
||||
const spec = this.savedObjectToSpec(savedObject);
|
||||
const { title, type, typeMeta } = spec;
|
||||
const parsedFieldFormats: FieldFormatMap = savedObject.attributes
|
||||
|
@ -512,7 +514,6 @@ export class IndexPatternsService {
|
|||
UI_SETTINGS.SHORT_DOTS_ENABLE
|
||||
);
|
||||
const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS);
|
||||
|
||||
const indexPattern = new IndexPattern({
|
||||
spec,
|
||||
savedObjectsClient: this.savedObjectsClient,
|
||||
|
@ -623,8 +624,12 @@ export class IndexPatternsService {
|
|||
version: indexPattern.version,
|
||||
})
|
||||
.then((resp) => {
|
||||
indexPattern.id = resp.id;
|
||||
indexPattern.version = resp.version;
|
||||
if (resp.id) {
|
||||
indexPattern.id = resp.id;
|
||||
}
|
||||
if (resp.version) {
|
||||
indexPattern.version = resp.version;
|
||||
}
|
||||
})
|
||||
.catch(async (err) => {
|
||||
if (
|
||||
|
|
|
@ -18,5 +18,6 @@
|
|||
*/
|
||||
|
||||
export const TAB_INDEXED_FIELDS = 'indexedFields';
|
||||
export const TAB_COMPLEX_FIELDS = 'complexFields';
|
||||
export const TAB_SCRIPTED_FIELDS = 'scriptedFields';
|
||||
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],
|
||||
};
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<IndexedFieldItem>> = [
|
||||
const columns: Array<EuiBasicTableColumn<IndexedFieldItem>> = this.props.columns || [
|
||||
{
|
||||
field: 'displayName',
|
||||
name: nameHeader,
|
||||
|
|
|
@ -34,6 +34,7 @@ import {
|
|||
EuiFieldSearch,
|
||||
EuiSelect,
|
||||
EuiSelectOption,
|
||||
EuiButton,
|
||||
} from "@elastic/eui";
|
||||
// import { fieldWildcardMatcher } from '../../../../../utils/public';
|
||||
import {
|
||||
|
@ -46,6 +47,7 @@ import {
|
|||
// import { IndexPatternManagmentContext } from '../../../types';
|
||||
import { createEditIndexPatternPageStateContainer } from "../edit_index_pattern_state_container";
|
||||
import {
|
||||
TAB_COMPLEX_FIELDS,
|
||||
TAB_INDEXED_FIELDS,
|
||||
TAB_SCRIPTED_FIELDS,
|
||||
TAB_SOURCE_FILTERS,
|
||||
|
@ -64,6 +66,7 @@ import {
|
|||
import { useGlobalContext } from "../../../context";
|
||||
|
||||
import LayoutList from "@/pages/DataManagement/View/LayoutList"
|
||||
import { ComplexFieldsTable } from "../indexed_fields_table/complex_fields_table";
|
||||
|
||||
interface TabsProps extends Pick<RouteComponentProps, "history" | "location"> {
|
||||
indexPattern: IndexPattern;
|
||||
|
@ -95,6 +98,7 @@ export function Tabs({
|
|||
indexPatternFieldEditor,
|
||||
} = useGlobalContext();
|
||||
const [fieldFilter, setFieldFilter] = useState<string>("");
|
||||
const [complexFieldFilter, setComplexFieldFilter] = 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(
|
||||
(type: string) => {
|
||||
switch (type) {
|
||||
|
@ -217,6 +254,25 @@ export function Tabs({
|
|||
/>
|
||||
</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:
|
||||
return (
|
||||
<Fragment>
|
||||
|
@ -261,6 +317,7 @@ export function Tabs({
|
|||
fieldWildcardMatcherDecorated,
|
||||
fields,
|
||||
getFilterSection,
|
||||
getComplexFilterSection,
|
||||
history,
|
||||
indexPattern,
|
||||
indexPatternManagementStart.list.getFieldInfo,
|
||||
|
@ -272,20 +329,30 @@ export function Tabs({
|
|||
);
|
||||
|
||||
const euiTabs: EuiTabbedContentTab[] = useMemo(
|
||||
() =>
|
||||
getTabs(indexPattern, fieldFilter, indexPatternManagementStart.list).map(
|
||||
() => {
|
||||
const tabs = getTabs(indexPattern, fieldFilter, indexPatternManagementStart.list).map(
|
||||
(tab: Pick<EuiTabbedContentTab, "name" | "id">) => {
|
||||
return {
|
||||
...tab,
|
||||
content: getContent(tab.id),
|
||||
};
|
||||
}
|
||||
).concat([{
|
||||
name: 'Layout',
|
||||
id: 'layout',
|
||||
content: <LayoutList indexPattern={indexPattern} clusterId={selectedCluster.id}/>,
|
||||
}]),
|
||||
[fieldFilter, getContent, indexPattern, indexPatternManagementStart.list]
|
||||
)
|
||||
let count = indexPattern?.complexFields?.length || 0
|
||||
if (complexFieldFilter) {
|
||||
const normalizedFieldFilter = complexFieldFilter.toLowerCase();
|
||||
const fields = indexPattern?.complexFields?.filter((field) =>
|
||||
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);
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import React, { PureComponent, Fragment } from 'react';
|
||||
import { intersection, union, get } from 'lodash';
|
||||
import React, { PureComponent, Fragment, useState, useMemo, useCallback } from 'react';
|
||||
import { intersection, union, get, cloneDeep } from 'lodash';
|
||||
|
||||
import {
|
||||
EuiBasicTable,
|
||||
|
@ -41,6 +41,13 @@ import {
|
|||
EuiSpacer,
|
||||
EuiText,
|
||||
EUI_MODAL_CONFIRM_BUTTON,
|
||||
EuiFilterButton,
|
||||
EuiFilterGroup,
|
||||
EuiPopover,
|
||||
EuiSelectable,
|
||||
EuiPopoverTitle,
|
||||
EuiBadge,
|
||||
EuiComboBox,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import {
|
||||
|
@ -69,10 +76,32 @@ import { IndexPatternManagmentContextValue } from '../../types';
|
|||
|
||||
import { FIELD_TYPES_BY_LANG, DEFAULT_FIELD_TYPES } from './constants';
|
||||
import { executeScript, isScriptValid } from './lib';
|
||||
import styles from './field_editor.less'
|
||||
|
||||
// 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 { 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 = (
|
||||
field: IndexPatternField['spec'],
|
||||
|
@ -206,7 +235,7 @@ export class FieldEditor extends PureComponent<FieldEdiorProps, FieldEditorState
|
|||
data.fieldFormats
|
||||
),
|
||||
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;
|
||||
}
|
||||
|
||||
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() {
|
||||
const { isReady, isCreating, spec } = this.state;
|
||||
|
||||
return isReady ? (
|
||||
<div>
|
||||
<div className={styles.editor}>
|
||||
<EuiText>
|
||||
<h3>
|
||||
{isCreating ? (
|
||||
|
@ -774,6 +864,7 @@ export class FieldEditor extends PureComponent<FieldEdiorProps, FieldEditorState
|
|||
{this.renderFormat()}
|
||||
{this.renderPopularity()}
|
||||
{this.renderScript()}
|
||||
{this.renderMetricConfig()}
|
||||
{this.renderActions()}
|
||||
{this.renderDeleteModal()}
|
||||
</EuiForm>
|
||||
|
@ -782,3 +873,140 @@ export class FieldEditor extends PureComponent<FieldEdiorProps, FieldEditorState
|
|||
) : 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.C": "Colleague C",
|
||||
"form.button.search": "Search",
|
||||
"form.button.apply": "Apply",
|
||||
"form.button.new": "New",
|
||||
"form.button.create": "Create",
|
||||
"form.button.add": "Add",
|
||||
|
|
|
@ -119,6 +119,10 @@ export default {
|
|||
"cluster.monitor.timepicker.lastyear": "Last year",
|
||||
"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.search_throughput.title": "Search Rate",
|
||||
"cluster.metrics.axis.index_latency.title": "Indexing Latency",
|
||||
|
|
|
@ -86,6 +86,7 @@ export default {
|
|||
"form.logstash.kafkaconf.label": "Logstash Kafka 配置",
|
||||
"form.logstash.kafkaconf.placeholder": "请输入Kafka配置",
|
||||
"form.button.search": "搜索",
|
||||
"form.button.apply": "应用",
|
||||
"form.button.new": "新建",
|
||||
"form.button.create": "创建",
|
||||
"form.button.add": "添加",
|
||||
|
|
|
@ -110,6 +110,10 @@ export default {
|
|||
"cluster.monitor.timepicker.lastyear": "最近1年",
|
||||
"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.search_throughput.title": "查询吞吐",
|
||||
"cluster.metrics.axis.index_latency.title": "索引延迟",
|
||||
|
|
|
@ -112,15 +112,15 @@ const Discover = (props) => {
|
|||
|
||||
const [timeZone, setTimeZone] = useState(() => getTimezone());
|
||||
const [mode, setMode] = useState("table");
|
||||
const [viewLayout, setViewLayout] = useState();
|
||||
// const [viewLayout, setViewLayout] = useState();
|
||||
const insightBarRef = useRef();
|
||||
const visRef = useRef();
|
||||
const layoutRef = useRef();
|
||||
// const layoutRef = useRef();
|
||||
const rangeCacheRef = useRef();
|
||||
const fullScreenHandle = useFullScreenHandle();
|
||||
const [layout, setLayout] = useState(
|
||||
Layouts.find((item) => item.name === "default")
|
||||
);
|
||||
// const [layout, setLayout] = useState(
|
||||
// Layouts.find((item) => item.name === "default")
|
||||
// );
|
||||
const [timeTipsLoading, setTimeTipsLoading] = useState(false);
|
||||
const [insightLoading, setInsightLoading] = useState(false);
|
||||
const [showResultCount, setShowResultCount] = useState(true);
|
||||
|
@ -293,9 +293,9 @@ const Discover = (props) => {
|
|||
getFilters()
|
||||
);
|
||||
}
|
||||
if (mode === "layout") {
|
||||
layoutRef?.current?.onRefresh();
|
||||
}
|
||||
// if (mode === "layout") {
|
||||
// layoutRef?.current?.onRefresh();
|
||||
// }
|
||||
if (!indexPatternRef.current) {
|
||||
return;
|
||||
}
|
||||
|
@ -415,7 +415,7 @@ const Discover = (props) => {
|
|||
columns: record.filter?.columns || ["_source"],
|
||||
}
|
||||
if (record.time_field) {
|
||||
newState.sort = [{[record.time_field]: {order: "desc"}}]
|
||||
newState.sort = [[record.time_field, 'desc']]
|
||||
}
|
||||
setState(newState);
|
||||
if (record.filter?.filters?.length > 0) {
|
||||
|
@ -881,24 +881,24 @@ const Discover = (props) => {
|
|||
}
|
||||
};
|
||||
|
||||
const fetchViewDefaultLayout = async (clusterId, viewId) => {
|
||||
setInsightLoading(true);
|
||||
const res = await request(
|
||||
`/elasticsearch/${clusterId}/saved_objects/view/${viewId}`
|
||||
);
|
||||
const layoutId = res?._source?.default_layout_id;
|
||||
if (layoutId) {
|
||||
const layout = await request(`/layout/${layoutId}`);
|
||||
if (layout?._source) {
|
||||
setViewLayout(layout?._source);
|
||||
} else {
|
||||
setViewLayout();
|
||||
}
|
||||
} else {
|
||||
setViewLayout();
|
||||
}
|
||||
setInsightLoading(false);
|
||||
};
|
||||
// const fetchViewDefaultLayout = async (clusterId, viewId) => {
|
||||
// setInsightLoading(true);
|
||||
// const res = await request(
|
||||
// `/elasticsearch/${clusterId}/saved_objects/view/${viewId}`
|
||||
// );
|
||||
// const layoutId = res?._source?.default_layout_id;
|
||||
// if (layoutId) {
|
||||
// const layout = await request(`/layout/${layoutId}`);
|
||||
// if (layout?._source) {
|
||||
// setViewLayout(layout?._source);
|
||||
// } else {
|
||||
// setViewLayout();
|
||||
// }
|
||||
// } else {
|
||||
// setViewLayout();
|
||||
// }
|
||||
// setInsightLoading(false);
|
||||
// };
|
||||
|
||||
const onFieldAgg = async (field, beforeFuc, afterFuc) => {
|
||||
let name = field?.spec?.name || field?.name
|
||||
|
@ -993,27 +993,27 @@ const Discover = (props) => {
|
|||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (indexPattern?.type === "view") {
|
||||
fetchViewDefaultLayout(props.selectedCluster?.id, indexPattern?.id);
|
||||
} else {
|
||||
setViewLayout();
|
||||
}
|
||||
}, [indexPattern, props.selectedCluster?.id]);
|
||||
// useEffect(() => {
|
||||
// if (indexPattern?.type === "view") {
|
||||
// fetchViewDefaultLayout(props.selectedCluster?.id, indexPattern?.id);
|
||||
// } else {
|
||||
// setViewLayout();
|
||||
// }
|
||||
// }, [indexPattern, props.selectedCluster?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
setMode(viewLayout ? "layout" : "table");
|
||||
}, [viewLayout]);
|
||||
// useEffect(() => {
|
||||
// setMode(viewLayout ? "layout" : "table");
|
||||
// }, [viewLayout]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode === "table" && viewLayout) {
|
||||
setViewLayout();
|
||||
}
|
||||
}, [mode]);
|
||||
// useEffect(() => {
|
||||
// if (mode === "table" && viewLayout) {
|
||||
// setViewLayout();
|
||||
// }
|
||||
// }, [mode]);
|
||||
|
||||
const showLayoutListIcon = useMemo(() => {
|
||||
return indexPattern?.type === "view";
|
||||
}, [indexPattern]);
|
||||
// const showLayoutListIcon = useMemo(() => {
|
||||
// return indexPattern?.type === "view";
|
||||
// }, [indexPattern]);
|
||||
|
||||
return (
|
||||
<div style={{ position: "relative" }}>
|
||||
|
@ -1104,10 +1104,10 @@ const Discover = (props) => {
|
|||
getBucketSize,
|
||||
columns: state.columns,
|
||||
}}
|
||||
layoutConfig={{
|
||||
layout,
|
||||
onChange: setLayout,
|
||||
}}
|
||||
// layoutConfig={{
|
||||
// layout,
|
||||
// onChange: setLayout,
|
||||
// }}
|
||||
isEmpty={resultState === "none" && queryFrom === 0}
|
||||
onQueriesSelect={onQueriesSelect}
|
||||
onQueriesRemove={(id) => {
|
||||
|
@ -1159,15 +1159,15 @@ const Discover = (props) => {
|
|||
break;
|
||||
}
|
||||
}}
|
||||
showLayoutListIcon={showLayoutListIcon}
|
||||
viewLayout={viewLayout}
|
||||
onViewLayoutChange={(layout) => {
|
||||
if (layout) {
|
||||
setViewLayout(layout);
|
||||
} else {
|
||||
setViewLayout();
|
||||
}
|
||||
}}
|
||||
showLayoutListIcon={false}
|
||||
// viewLayout={viewLayout}
|
||||
// onViewLayoutChange={(layout) => {
|
||||
// if (layout) {
|
||||
// setViewLayout(layout);
|
||||
// } else {
|
||||
// setViewLayout();
|
||||
// }
|
||||
// }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -1200,20 +1200,22 @@ const Discover = (props) => {
|
|||
getFilters={getSearchFilters}
|
||||
getBucketSize={getBucketSize}
|
||||
fullScreenHandle={fullScreenHandle}
|
||||
layout={layout}
|
||||
// layout={layout}
|
||||
selectedQueries={selectedQueries}
|
||||
/>
|
||||
) : mode === "layout" ? (
|
||||
<Layout
|
||||
ref={layoutRef}
|
||||
clusterId={props.selectedCluster?.id}
|
||||
indexPattern={indexPattern}
|
||||
timeRange={timefilter?.getTime()}
|
||||
query={getSearchFilters()}
|
||||
layout={viewLayout}
|
||||
fullScreenHandle={fullScreenHandle}
|
||||
/>
|
||||
) : (
|
||||
) :
|
||||
// mode === "layout" ? (
|
||||
// <Layout
|
||||
// ref={layoutRef}
|
||||
// clusterId={props.selectedCluster?.id}
|
||||
// indexPattern={indexPattern}
|
||||
// timeRange={timefilter?.getTime()}
|
||||
// query={getSearchFilters()}
|
||||
// layout={viewLayout}
|
||||
// fullScreenHandle={fullScreenHandle}
|
||||
// />
|
||||
// ) :
|
||||
(
|
||||
<>
|
||||
{indexPattern && (
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -19,6 +19,7 @@ import { formatMessage } from "umi/locale";
|
|||
import { getAuthority, hasAuthority } from "@/utils/authority";
|
||||
import EditLayout from "./View/EditLayout";
|
||||
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) => {
|
||||
if (!props.selectedCluster?.id) {
|
||||
|
@ -57,15 +58,11 @@ const IndexPatterns = (props) => {
|
|||
</Route>
|
||||
<Route
|
||||
path={[
|
||||
"/patterns/:id/layout/create",
|
||||
"/patterns/:id/layout/:layoutId/edit",
|
||||
"/patterns/:id/complex/:fieldName/edit",
|
||||
"/patterns/:id/complex/create/",
|
||||
]}
|
||||
>
|
||||
<EditLayout
|
||||
selectedCluster={props.selectedCluster}
|
||||
clusterList={props.clusterList}
|
||||
clusterStatus={props.clusterStatus}
|
||||
/>
|
||||
<CreateEditComplexFieldContainer selectedCluster={props.selectedCluster} />
|
||||
</Route>
|
||||
<Route
|
||||
path={[
|
||||
|
|
|
@ -38,18 +38,14 @@ export default (props) => {
|
|||
>
|
||||
<TopN type={param?.tab} {...props}/>
|
||||
</Tabs.TabPane>
|
||||
{
|
||||
!isAgent && (
|
||||
<Tabs.TabPane
|
||||
key="index"
|
||||
tab={formatMessage({
|
||||
id: "cluster.monitor.index.title",
|
||||
})}
|
||||
>
|
||||
<TopN type={param?.tab} {...props}/>
|
||||
</Tabs.TabPane>
|
||||
)
|
||||
}
|
||||
<Tabs.TabPane
|
||||
key="index"
|
||||
tab={formatMessage({
|
||||
id: "cluster.monitor.index.title",
|
||||
})}
|
||||
>
|
||||
<TopN type={param?.tab} {...props}/>
|
||||
</Tabs.TabPane>
|
||||
{
|
||||
isAgent && (
|
||||
<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 { useMemo } from "react";
|
||||
import { formatMessage } from "umi/locale";
|
||||
import { fixFormatter } from "./Treemap";
|
||||
import { fixFormatter } from "./Chart";
|
||||
|
||||
export default (props) => {
|
||||
|
||||
|
@ -23,8 +23,8 @@ export default (props) => {
|
|||
key: 'displayName',
|
||||
}];
|
||||
if (sourceArea) {
|
||||
const { format: formatArea, unit: unitArea } = sourceArea || {}
|
||||
const formatterArea = fixFormatter(formatArea)
|
||||
const { format: formatArea, pattern: patternArea, unit: unitArea } = sourceArea || {}
|
||||
const formatterArea = fixFormatter(formatArea, patternArea)
|
||||
newColumns.push({
|
||||
title: unitArea ? `${sourceArea.name}(${unitArea})` : sourceArea.name,
|
||||
dataIndex: 'value',
|
||||
|
@ -35,8 +35,8 @@ export default (props) => {
|
|||
})
|
||||
}
|
||||
if (sourceColor) {
|
||||
const { format: formatColor, unit: unitColor } = sourceColor
|
||||
const formatterColor = fixFormatter(formatColor)
|
||||
const { format: formatColor, pattern: patternColor, unit: unitColor } = sourceColor
|
||||
const formatterColor = fixFormatter(formatColor, patternColor)
|
||||
newColumns.push({
|
||||
title: unitColor ? `${sourceColor.name}(${unitColor})` : sourceColor.name,
|
||||
dataIndex: 'valueColor',
|
||||
|
|
|
@ -1,61 +1,7 @@
|
|||
import { getFormatter } from "@/utils/format";
|
||||
import { Treemap } from "@ant-design/charts";
|
||||
import { Empty } from "antd";
|
||||
import { useMemo } from "react";
|
||||
|
||||
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' : '')
|
||||
}
|
||||
import { fixFormatter, generateColors } from "./Chart";
|
||||
|
||||
export default (props) => {
|
||||
|
||||
|
@ -69,12 +15,12 @@ export default (props) => {
|
|||
|
||||
const color = useMemo(() => {
|
||||
if (colors.length === 0 || !sourceColor?.key || data.length === 0) return undefined
|
||||
const newColors = generateColors(colors, data)
|
||||
const sortData = data.sort((a, b) => a.valueColor - b.valueColor)
|
||||
const newColors = generateColors(colors, data).reverse()
|
||||
if (newColors.length === 0) return undefined
|
||||
return ({ name }) => {
|
||||
const index = sortData.findIndex((item) => item.name === name)
|
||||
const index = data.findIndex((item) => item.name === name)
|
||||
if (index !== -1) {
|
||||
return newColors[index] || newColors[0]
|
||||
return newColors[index] || (value == 0 ? newColors[newColors.length - 1] : newColors[0])
|
||||
} else {
|
||||
return newColors[0]
|
||||
}
|
||||
|
@ -83,12 +29,8 @@ export default (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>
|
||||
) : (
|
||||
<Treemap {...{
|
||||
<Treemap {...{
|
||||
animation: false,
|
||||
data: {
|
||||
name: 'root',
|
||||
children: data
|
||||
|
@ -96,17 +38,17 @@ export default (props) => {
|
|||
autoFit: true,
|
||||
color,
|
||||
colorField: 'name',
|
||||
legend: {
|
||||
position: 'top-left',
|
||||
itemName: {
|
||||
formatter: (text) => {
|
||||
const item = data.find((item) => item.name === text)
|
||||
return item?.groupName || text
|
||||
}
|
||||
}
|
||||
},
|
||||
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: {
|
||||
customContent: (title, items) => {
|
||||
|
@ -117,8 +59,8 @@ export default (props) => {
|
|||
const markers = []
|
||||
|
||||
if (metricArea && tooltipArea !== false) {
|
||||
const { format: formatArea, unit: unitArea } = sourceArea || {}
|
||||
const formatterArea = fixFormatter(formatArea)
|
||||
const { format: formatArea, pattern: patternArea, unit: unitArea } = sourceArea || {}
|
||||
const formatterArea = fixFormatter(formatArea, patternArea)
|
||||
markers.push({
|
||||
name: nameArea,
|
||||
value: formatterArea ? formatterArea(value) : value,
|
||||
|
@ -128,8 +70,8 @@ export default (props) => {
|
|||
}
|
||||
|
||||
if (metricColor) {
|
||||
const { format: formatColor, unit: unitColor } = sourceColor || {}
|
||||
const formatterColor = fixFormatter(formatColor)
|
||||
const { format: formatColor, pattern: patternColor, unit: unitColor } = sourceColor || {}
|
||||
const formatterColor = fixFormatter(formatColor, patternColor)
|
||||
markers.push({
|
||||
name: nameColor,
|
||||
value: formatterColor ? formatterColor(valueColor) : valueColor,
|
||||
|
@ -170,7 +112,6 @@ export default (props) => {
|
|||
},
|
||||
}
|
||||
}} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -6,63 +6,146 @@ import { formatMessage } from "umi/locale";
|
|||
import ConvertSvg from "@/components/Icons/Convert"
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import ColorPicker from "./ColorPicker";
|
||||
import Treemap from "./Treemap";
|
||||
import Chart from "./Chart";
|
||||
import Table from "./Table";
|
||||
import GradientColorPicker from "./GradientColorPicker";
|
||||
import { cloneDeep } from "lodash";
|
||||
import request from "@/utils/request";
|
||||
import { formatTimeRange } from "@/lib/elasticsearch/util";
|
||||
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) => {
|
||||
|
||||
const { type, clusterID, timeRange } = props;
|
||||
const { type, clusterID, timeRange, isAgent } = props;
|
||||
|
||||
const [currentMode, setCurrentMode] = useState('treemap')
|
||||
|
||||
const [metrics, setMetrics] = useState([])
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
top: 15,
|
||||
colors: ['#00bb1b', '#fcca00', '#ff4d4f']
|
||||
top: DEFAULT_TOP,
|
||||
colors: DEFAULT_COLORS
|
||||
})
|
||||
|
||||
const isRollupEnabled = getRollupEnabled() === 'true'
|
||||
|
||||
const [config, setConfig] = useState({})
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [data, setData] = useState([])
|
||||
const [result, setResult] = useState()
|
||||
const [selectedView, setSelectedView] = useState()
|
||||
const searchParamsRef = useRef()
|
||||
|
||||
const fetchMetrics = async (type) => {
|
||||
const fetchFields = async (clusterID, viewID, type, isAgent) => {
|
||||
if (!clusterID || !viewID) return;
|
||||
setLoading(true)
|
||||
const res = await request(`/collection/metric/_search`, {
|
||||
const res = await request(`/elasticsearch/${clusterID}/saved_objects/_bulk_get`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
size: 10000,
|
||||
from: 0,
|
||||
query: { bool: { filter: [{ "term": { "level": type === 'index' ? 'indices' : type } }] }}
|
||||
}
|
||||
body: [{
|
||||
id: viewID,
|
||||
type: "view"
|
||||
}]
|
||||
})
|
||||
if (res?.hits?.hits) {
|
||||
const newMetrics = res?.hits?.hits.filter((item) => {
|
||||
const { items = [] } = item._source;
|
||||
if (items.length === 0) return false
|
||||
return true;
|
||||
}).map((item) => ({ ...item._source }))
|
||||
setMetrics(newMetrics)
|
||||
if (newMetrics.length > 0 && (!formData.sourceArea && !formData.sourceColor)) {
|
||||
if (res && !res.error && Array.isArray(res.saved_objects) && res.saved_objects[0]) {
|
||||
const newView = res.saved_objects[0]
|
||||
let { fieldFormatMap, fields } = newView.attributes || {}
|
||||
let { complex_fields: complexFields } = newView
|
||||
try {
|
||||
fieldFormatMap = JSON.parse(fieldFormatMap) || {}
|
||||
fields = JSON.parse(fields) || []
|
||||
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 = {
|
||||
...cloneDeep(formData),
|
||||
sourceArea: newMetrics[0],
|
||||
statisticArea: newMetrics[0]?.items[0]?.statistic,
|
||||
sourceColor: newMetrics[0],
|
||||
statisticColor: newMetrics[0].items[0]?.statistic
|
||||
sourceArea: initField,
|
||||
statisticArea: initStatistic,
|
||||
sourceColor: initField,
|
||||
statisticColor: initStatistic
|
||||
}
|
||||
setFormData(newFormData)
|
||||
fetchData(type, clusterID, timeRange, newFormData)
|
||||
}
|
||||
} else {
|
||||
setFormData({
|
||||
top: DEFAULT_TOP,
|
||||
colors: DEFAULT_COLORS
|
||||
})
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
@ -72,20 +155,94 @@ export default (props) => {
|
|||
if (shouldLoading) {
|
||||
setLoading(true)
|
||||
}
|
||||
const { top, sourceArea = {}, statisticArea, statisticColor, sourceColor = {} } = formData
|
||||
const { top, sourceArea, statisticArea, statisticColor, sourceColor } = formData
|
||||
const newTimeRange = formatTimeRange(timeRange);
|
||||
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 = {
|
||||
"index_pattern": ".infini_metrics*",
|
||||
"time_field": "timestamp",
|
||||
"bucket_size": "auto",
|
||||
"filter": {
|
||||
"bool": {
|
||||
"must": [{
|
||||
"term": {
|
||||
"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),
|
||||
"items": [...(sourceArea?.items || [])?.map((item) => {
|
||||
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),
|
||||
"formulas": formulas,
|
||||
"items": items,
|
||||
"groups": [{
|
||||
"field": type === 'shard' ? `metadata.labels.shard_id` : `metadata.labels.${type}_name`,
|
||||
"limit": top
|
||||
|
@ -139,9 +284,9 @@ export default (props) => {
|
|||
"key": sortKey
|
||||
}] : undefined
|
||||
}
|
||||
if (statisticArea !== 'rate' && statisticColor !== 'rate') {
|
||||
delete body['time_field']
|
||||
delete body['bucket_size']
|
||||
if ((isAreaRate || isColorRate) || (isAreaLatency || isColorLatency)) {
|
||||
body['time_field'] = "timestamp"
|
||||
body['bucket_size'] = "auto"
|
||||
}
|
||||
const res = await request(`/elasticsearch/infini_default_system_cluster/visualization/data`, {
|
||||
method: 'POST',
|
||||
|
@ -149,7 +294,28 @@ export default (props) => {
|
|||
})
|
||||
if (res && !res.error) {
|
||||
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 {
|
||||
setResult()
|
||||
}
|
||||
|
@ -179,7 +345,10 @@ export default (props) => {
|
|||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchMetrics(type)
|
||||
fetchFields(clusterID, 'infini_metrics', type, isAgent)
|
||||
}, [clusterID, type, isAgent])
|
||||
|
||||
useEffect(() => {
|
||||
}, [type])
|
||||
|
||||
const isTreemap = useMemo(() => {
|
||||
|
@ -214,7 +383,7 @@ export default (props) => {
|
|||
sortKey = 'value'
|
||||
} else {
|
||||
if (sourceColor) {
|
||||
const key = uuid.v4();
|
||||
const key = generate20BitUUID();
|
||||
object['metricArea'] = `metric_${key}`
|
||||
object['value'] = 1
|
||||
object['nameArea'] = `name_${key}`
|
||||
|
@ -270,12 +439,9 @@ export default (props) => {
|
|||
/>
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
<Input
|
||||
style={{ width: "60px", marginBottom: 12 }}
|
||||
className={styles.borderRadiusLeft}
|
||||
disabled
|
||||
defaultValue={"Top"}
|
||||
/>
|
||||
<div className={styles.label}>
|
||||
Top
|
||||
</div>
|
||||
<InputNumber
|
||||
style={{ width: "80px", marginBottom: 12, marginRight: 12 }}
|
||||
className={styles.borderRadiusRight}
|
||||
|
@ -285,22 +451,25 @@ export default (props) => {
|
|||
precision={0}
|
||||
onChange={(value) => onFormDataChange({ top: value })}
|
||||
/>
|
||||
<Input
|
||||
style={{ width: "80px", marginBottom: 12 }}
|
||||
className={styles.borderRadiusLeft}
|
||||
disabled
|
||||
defaultValue={"面积指标"}
|
||||
/>
|
||||
<div className={styles.label}>
|
||||
{formatMessage({ id: "cluster.monitor.topn.area" })}
|
||||
</div>
|
||||
<Select
|
||||
style={{ width: "150px", marginBottom: 12 }}
|
||||
value={formData.sourceArea?.key}
|
||||
value={formData.sourceArea?.name}
|
||||
dropdownMatchSelectWidth={false}
|
||||
onChange={(value, option) => {
|
||||
|
||||
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({
|
||||
statisticArea: items[0]?.statistic === 'derivative' ? 'rate' : items[0]?.statistic,
|
||||
statisticArea: statisticArea,
|
||||
sourceArea: option?.props?.metric
|
||||
})
|
||||
} else {
|
||||
|
@ -313,9 +482,9 @@ export default (props) => {
|
|||
allowClear
|
||||
>
|
||||
{
|
||||
metrics.map((item) => (
|
||||
<Select.Option key={item.key} metric={item}>
|
||||
{item.name}
|
||||
(selectedView?.fields || []).filter((item) => !!item.metric_config).map((item) => (
|
||||
<Select.Option key={item.name} metric={item}>
|
||||
{item.metric_config.name}
|
||||
</Select.Option>
|
||||
))
|
||||
}
|
||||
|
@ -328,30 +497,36 @@ export default (props) => {
|
|||
onChange={(value) => onFormDataChange({ statisticArea: value })}
|
||||
>
|
||||
{
|
||||
formData.sourceArea?.statistics?.filter((item) => !!item).map((item) => (
|
||||
<Select.Option key={item}>
|
||||
{item.toUpperCase()}
|
||||
</Select.Option>
|
||||
))
|
||||
formData.sourceArea?.metric_config?.option_aggs?.filter((item) => !!item).map((item) => {
|
||||
const limits = ROLLUP_FIELDS[formData.sourceArea.name]
|
||||
return (
|
||||
<Select.Option key={item} disabled={limits && isRollupEnabled ? !limits.includes(item) : false}>
|
||||
{item.toUpperCase()}
|
||||
</Select.Option>
|
||||
)
|
||||
})
|
||||
}
|
||||
</Select>
|
||||
<Button style={{ width: 32, marginBottom: 12, padding: 0, marginRight: 6, borderRadius: 4 }} onClick={() => onMetricExchange()}><Icon style={{ fontSize: 16 }} component={ConvertSvg}/></Button>
|
||||
<Input
|
||||
style={{ width: "80px", marginBottom: 12 }}
|
||||
className={styles.borderRadiusLeft}
|
||||
disabled
|
||||
defaultValue={"颜色指标"}
|
||||
/>
|
||||
|
||||
<div className={styles.label}>
|
||||
{formatMessage({ id: "cluster.monitor.topn.color" })}
|
||||
</div>
|
||||
<Select
|
||||
style={{ width: "150px", marginBottom: 12 }}
|
||||
value={formData.sourceColor?.key}
|
||||
value={formData.sourceColor?.name}
|
||||
dropdownMatchSelectWidth={false}
|
||||
onChange={(value, option) => {
|
||||
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({
|
||||
statisticColor: items[0]?.statistic === 'derivative' ? 'rate' : items[0]?.statistic,
|
||||
statisticColor: statisticColor,
|
||||
sourceColor: option?.props?.metric
|
||||
})
|
||||
} else {
|
||||
|
@ -365,9 +540,9 @@ export default (props) => {
|
|||
allowClear
|
||||
>
|
||||
{
|
||||
metrics.map((item) => (
|
||||
<Select.Option key={item.key} metric={item}>
|
||||
{item.name}
|
||||
(selectedView?.fields || []).filter((item) => !!item.metric_config).map((item) => (
|
||||
<Select.Option key={item.name} metric={item}>
|
||||
{item.metric_config.name}
|
||||
</Select.Option>
|
||||
))
|
||||
}
|
||||
|
@ -379,18 +554,19 @@ export default (props) => {
|
|||
onChange={(value) => onFormDataChange({ statisticColor: value })}
|
||||
>
|
||||
{
|
||||
formData.sourceColor?.statistics?.filter((item) => !!item).map((item) => (
|
||||
<Select.Option key={item}>
|
||||
{item.toUpperCase()}
|
||||
</Select.Option>
|
||||
))
|
||||
formData.sourceColor?.metric_config?.option_aggs?.filter((item) => !!item).map((item) => {
|
||||
const limits = ROLLUP_FIELDS[formData.sourceColor.name]
|
||||
return (
|
||||
<Select.Option key={item} disabled={limits && isRollupEnabled ? !limits.includes(item) : false}>
|
||||
{item.toUpperCase()}
|
||||
</Select.Option>
|
||||
)
|
||||
})
|
||||
}
|
||||
</Select>
|
||||
<Input
|
||||
style={{ width: "60px", marginBottom: 12 }}
|
||||
disabled
|
||||
defaultValue={"主题"}
|
||||
/>
|
||||
<div className={styles.label} style={{ borderTopLeftRadius: 0, borderBottomLeftRadius: 0 }}>
|
||||
{formatMessage({ id: "cluster.monitor.topn.theme" })}
|
||||
</div>
|
||||
<GradientColorPicker className={styles.borderRadiusRight} style={{ marginRight: 12, marginBottom: 12 }} value={formData.colors || []} onChange={(value) => {
|
||||
onFormDataChange({ colors: value })
|
||||
setConfig({
|
||||
|
@ -398,7 +574,7 @@ export default (props) => {
|
|||
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>
|
||||
</div>
|
||||
|
||||
|
@ -414,7 +590,7 @@ export default (props) => {
|
|||
</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>
|
||||
</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 {
|
||||
|
|
|
@ -72,9 +72,14 @@ export function getAuthorizationHeader() {
|
|||
return "";
|
||||
}
|
||||
|
||||
export function getRollupEnabled() {
|
||||
return localStorage.getItem("infini-rollup-enabled");
|
||||
}
|
||||
|
||||
(async function() {
|
||||
const authRes = await request("/setting/application");
|
||||
if (authRes && !authRes.error) {
|
||||
localStorage.setItem("infini-auth", authRes.auth_enabled);
|
||||
const res = await request("/setting/application");
|
||||
if (res && !res.error) {
|
||||
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 '-';
|
||||
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