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:
yaojp123 2025-01-23 20:03:02 +08:00 committed by GitHub
parent 16c08e745f
commit d828fe8781
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 2438 additions and 352 deletions

View File

@ -531,6 +531,7 @@ const Index = forwardRef((props, ref) => {
<WidgetRender
widget={histogramState.widget}
range={histogramState.range}
queryParams={queryParams?.filters || {}}
/>
</div>
) : null}

View File

@ -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} />
}

View File

@ -0,0 +1,7 @@
.tabs {
:global {
.ant-tabs .ant-tabs-right-content {
padding-right: 16px !important;
}
}
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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,
};
}
}

View File

@ -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,

View File

@ -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 (

View File

@ -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';

View File

@ -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>
);
}
);

View File

@ -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);

View File

@ -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';

View File

@ -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>
);
}
}

View File

@ -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,

View File

@ -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);

View File

@ -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;
}
}
}

View File

@ -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" />
&nbsp;
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;
}
}

View File

@ -0,0 +1,8 @@
.editor {
background: #fff;
:global {
.euiBadge__content {
font-weight: 400;
}
}
}

View File

@ -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>
);
};

View File

@ -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

View File

@ -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>
</>
)
}

View File

@ -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> */}
</>
)
}

View File

@ -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>
)
}

View File

@ -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> */}
</>
)
}

View File

@ -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> */}
</>
)
}

View File

@ -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",

View File

@ -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",

View File

@ -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": "添加",

View File

@ -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": "索引延迟",

View File

@ -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}>

View File

@ -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={[

View File

@ -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

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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',

View File

@ -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>
)
}

View File

@ -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>

View File

@ -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 {

View File

@ -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)
}
})();

View File

@ -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);
}