diff --git a/web/src/components/ListView/index.jsx b/web/src/components/ListView/index.jsx index 175f041d..8732c8bc 100644 --- a/web/src/components/ListView/index.jsx +++ b/web/src/components/ListView/index.jsx @@ -531,6 +531,7 @@ const Index = forwardRef((props, ref) => { ) : null} diff --git a/web/src/components/Overview/Monitor/index.jsx b/web/src/components/Overview/Monitor/index.jsx index ecbf8a7a..07eec0fc 100644 --- a/web/src/components/Overview/Monitor/index.jsx +++ b/web/src/components/Overview/Monitor/index.jsx @@ -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) => {
- + { selectedCluster?.id ? ( <> @@ -187,58 +188,59 @@ const Monitor = (props) => {
- - { - setParam({ ...param, tab: key }); - }} - tabBarGutter={10} - destroyInactiveTabPane - animated={false} - > - {panes.map((pane) => ( - - - - -
- {checkPaneParams({ - ...state, - ...extraParams, - }) ? ( - typeof pane.component == "string" ? ( - pane.component - ) : ( - { - onTimeSettingsChange({ - timeInterval, - }) - setState({ - ...state, - timeInterval, - }); - }} - setSpinning={setSpinning} - {...extraParams} - bucketSize={state.timeInterval} - /> - ) - ) : null} -
-
- ))} -
+
+ { + setParam({ ...param, tab: key }); + }} + tabBarGutter={10} + destroyInactiveTabPane + animated={false} + > + {panes.map((pane) => ( + + + + +
+ {checkPaneParams({ + ...state, + ...extraParams, + }) ? ( + typeof pane.component == "string" ? ( + pane.component + ) : ( + { + onTimeSettingsChange({ + timeInterval, + }) + setState({ + ...state, + timeInterval, + }); + }} + setSpinning={setSpinning} + {...extraParams} + bucketSize={state.timeInterval} + /> + ) + ) : null} +
+
+ ))} +
+
) : } diff --git a/web/src/components/Overview/Monitor/index.less b/web/src/components/Overview/Monitor/index.less new file mode 100644 index 00000000..8d95fe64 --- /dev/null +++ b/web/src/components/Overview/Monitor/index.less @@ -0,0 +1,7 @@ +.tabs { + :global { + .ant-tabs .ant-tabs-right-content { + padding-right: 16px !important; + } + } +} \ No newline at end of file diff --git a/web/src/components/vendor/core/public/saved_objects/simple_saved_object.ts b/web/src/components/vendor/core/public/saved_objects/simple_saved_object.ts index 7d94afaa..4aa5fcf0 100644 --- a/web/src/components/vendor/core/public/saved_objects/simple_saved_object.ts +++ b/web/src/components/vendor/core/public/saved_objects/simple_saved_object.ts @@ -43,7 +43,7 @@ export class SimpleSavedObject { constructor( private client: SavedObjectsClientContract, - { id, type, version, attributes, error, references, migrationVersion }: SavedObjectType + { id, type, version, attributes, error, references, migrationVersion, complex_fields }: SavedObjectType ) { this.id = id; this.type = type; @@ -51,6 +51,7 @@ export class SimpleSavedObject { this.references = references || []; this._version = version; this.migrationVersion = migrationVersion; + this.attributes.complexFields = complex_fields if (error) { this.error = error; } diff --git a/web/src/components/vendor/data/common/index_patterns/fields/field_list.ts b/web/src/components/vendor/data/common/index_patterns/fields/field_list.ts index c0eb55a1..0187d967 100644 --- a/web/src/components/vendor/data/common/index_patterns/fields/field_list.ts +++ b/web/src/components/vendor/data/common/index_patterns/fields/field_list.ts @@ -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); diff --git a/web/src/components/vendor/data/common/index_patterns/fields/index_pattern_field.ts b/web/src/components/vendor/data/common/index_patterns/fields/index_pattern_field.ts index 4a22508f..35b989fa 100644 --- a/web/src/components/vendor/data/common/index_patterns/fields/index_pattern_field.ts +++ b/web/src/components/vendor/data/common/index_patterns/fields/index_pattern_field.ts @@ -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, }; } } diff --git a/web/src/components/vendor/data/common/index_patterns/index_patterns/index_pattern.ts b/web/src/components/vendor/data/common/index_patterns/index_patterns/index_pattern.ts index 9050b2ae..6f340963 100644 --- a/web/src/components/vendor/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/web/src/components/vendor/data/common/index_patterns/index_patterns/index_pattern.ts @@ -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>( + (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, diff --git a/web/src/components/vendor/data/common/index_patterns/index_patterns/index_patterns.ts b/web/src/components/vendor/data/common/index_patterns/index_patterns/index_patterns.ts index 4e63f4a0..f85d8085 100644 --- a/web/src/components/vendor/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/web/src/components/vendor/data/common/index_patterns/index_patterns/index_patterns.ts @@ -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 ( diff --git a/web/src/components/vendor/index_pattern_management/public/components/edit_index_pattern/constants.ts b/web/src/components/vendor/index_pattern_management/public/components/edit_index_pattern/constants.ts index 56da031e..a0354999 100644 --- a/web/src/components/vendor/index_pattern_management/public/components/edit_index_pattern/constants.ts +++ b/web/src/components/vendor/index_pattern_management/public/components/edit_index_pattern/constants.ts @@ -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'; diff --git a/web/src/components/vendor/index_pattern_management/public/components/edit_index_pattern/create_edit_complex_field/create_edit_complex_field.tsx b/web/src/components/vendor/index_pattern_management/public/components/edit_index_pattern/create_edit_complex_field/create_edit_complex_field.tsx new file mode 100644 index 00000000..b75c4be8 --- /dev/null +++ b/web/src/components/vendor/index_pattern_management/public/components/edit_index_pattern/create_edit_complex_field/create_edit_complex_field.tsx @@ -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 ( + + + + + + + + + + + ); + } +); diff --git a/web/src/components/vendor/index_pattern_management/public/components/edit_index_pattern/create_edit_complex_field/create_edit_complex_field_container.tsx b/web/src/components/vendor/index_pattern_management/public/components/edit_index_pattern/create_edit_complex_field/create_edit_complex_field_container.tsx new file mode 100644 index 00000000..84c2f576 --- /dev/null +++ b/web/src/components/vendor/index_pattern_management/public/components/edit_index_pattern/create_edit_complex_field/create_edit_complex_field_container.tsx @@ -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 = ({ ...props }) => { + // const { setBreadcrumbs, data } = useKibana().services; + const {data} = useGlobalContext() + const [indexPattern, setIndexPattern] = useState(); + + 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 ( + + ); + } else { + return <>; + } +}; + +export const CreateEditComplexFieldContainer = withRouter(CreateEditFieldCont); diff --git a/web/src/components/vendor/index_pattern_management/public/components/edit_index_pattern/create_edit_complex_field/index.ts b/web/src/components/vendor/index_pattern_management/public/components/edit_index_pattern/create_edit_complex_field/index.ts new file mode 100644 index 00000000..8b31ec2b --- /dev/null +++ b/web/src/components/vendor/index_pattern_management/public/components/edit_index_pattern/create_edit_complex_field/index.ts @@ -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'; diff --git a/web/src/components/vendor/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/complex_fields_table.jsx b/web/src/components/vendor/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/complex_fields_table.jsx new file mode 100644 index 00000000..79564cc2 --- /dev/null +++ b/web/src/components/vendor/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/complex_fields_table.jsx @@ -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 ( +
+ + 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', + }, + ]} + /> + + + ); + } +} diff --git a/web/src/components/vendor/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx b/web/src/components/vendor/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx index fc8320c1..35d2888c 100644 --- a/web/src/components/vendor/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx +++ b/web/src/components/vendor/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx @@ -139,7 +139,7 @@ export class Table extends PureComponent { pageSizeOptions: [5, 10, 25, 50], }; - const columns: Array> = [ + const columns: Array> = this.props.columns || [ { field: 'displayName', name: nameHeader, diff --git a/web/src/components/vendor/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx b/web/src/components/vendor/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx index 18701556..c57ed6f8 100644 --- a/web/src/components/vendor/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx +++ b/web/src/components/vendor/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx @@ -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 { indexPattern: IndexPattern; @@ -95,6 +98,7 @@ export function Tabs({ indexPatternFieldEditor, } = useGlobalContext(); const [fieldFilter, setFieldFilter] = useState(""); + const [complexFieldFilter, setComplexFieldFilter] = useState(""); const [indexedFieldTypeFilter, setIndexedFieldTypeFilter] = useState( "" ); @@ -193,6 +197,39 @@ export function Tabs({ ] ); + const getComplexFilterSection = useCallback( + () => { + return ( + + + setComplexFieldFilter(e.target.value)} + data-test-subj="complexFieldFilter" + aria-label={searchAriaLabel} + /> + + + { + history.push(`/patterns/${indexPattern?.id}/complex/create`); + }} + > + {"Create field"} + + + + ); + }, + [ + complexFieldFilter, + indexPattern, + ] + ); + const getContent = useCallback( (type: string) => { switch (type) { @@ -217,6 +254,25 @@ export function Tabs({ /> ); + case TAB_COMPLEX_FIELDS: + return ( + + + {getComplexFilterSection()} + + { + history.push(`/patterns/${indexPattern?.id}/complex/${field?.name}/edit`); + }, + getFieldInfo: indexPatternManagementStart.list.getFieldInfo, + }} + /> + + ); case TAB_SCRIPTED_FIELDS: return ( @@ -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) => { return { ...tab, content: getContent(tab.id), }; } - ).concat([{ - name: 'Layout', - id: 'layout', - content: , - }]), - [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); diff --git a/web/src/components/vendor/index_pattern_management/public/components/field_editor/complex_field_editor.less b/web/src/components/vendor/index_pattern_management/public/components/field_editor/complex_field_editor.less new file mode 100644 index 00000000..aab55d3d --- /dev/null +++ b/web/src/components/vendor/index_pattern_management/public/components/field_editor/complex_field_editor.less @@ -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; + } + } +} \ No newline at end of file diff --git a/web/src/components/vendor/index_pattern_management/public/components/field_editor/complex_field_editor.tsx b/web/src/components/vendor/index_pattern_management/public/components/field_editor/complex_field_editor.tsx new file mode 100644 index 00000000..896c943e --- /dev/null +++ b/web/src/components/vendor/index_pattern_management/public/components/field_editor/complex_field_editor.tsx @@ -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 { + // 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 ? ( + + +   + You already have a field with the name {spec.name}. + + ) : null + } + isInvalid={isInvalid} + error={ + isInvalid + ? 'Name is required' + : null + } + > + { + this.onFieldChange('name', e.target.value); + }} + isInvalid={isInvalid} + /> + + ) : 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: {defaultFormat}) + ) : ( + "Format" + ); + + return ( + + + { + return { value: fmt.id || '', text: fmt.title }; + })} + data-test-subj="editorSelectedFormatId" + onChange={(e) => { + this.onFormatChange(e.target.value); + }} + /> + + {fieldFormatId ? ( + + ) : null} + + ); + } + + renderDeleteModal = () => { + const { spec } = this.state; + + return this.state.showDeleteModal ? ( + + { + this.hideDeleteModal(); + this.deleteField(); + }} + cancelButtonText='Cancel' + confirmButtonText= 'Delete' + buttonColor="danger" + defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + > +

+ You can't recover a deleted field. +
+
+
Are you sure you want to do this? +

+
+
+ ) : 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 ( + + + + + {isCreating ? ( + "Create field" + ) : ( + "Save field" + )} + + + + + Cancel + + + {!isCreating ? ( + + + + + Delete + + + + + ) : null} + + + ); + } + + 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 ( + <> + + { + this.onFieldChange('metric_name', e.target.value) + }} + /> + + + ({ value: item, text: item.toUpperCase() }))} + value={statistic} + onChange={(e) => { + this.onFieldChange('function', { [e.target.value]: {} }) + }} + /> + + {this.renderFunction(statistic || 'rate')} + {this.renderFormat()} + + { + this.onFieldChange('unit', e.target.value) + }} + /> + + + { + this.onFieldChange('tags', value) + }}/> + + + ); + } + + render() { + const { isReady, isCreating, spec } = this.state; + + return isReady ? ( +
+ +

+ {isCreating ? ( + "Create field" + ) : ( + `Edit ${spec.metric_name }` + )} +

+
+ + + {this.renderMetricConfig()} + {this.renderActions()} + {this.renderDeleteModal()} + + +
+ ) : null; + } +} diff --git a/web/src/components/vendor/index_pattern_management/public/components/field_editor/field_editor.less b/web/src/components/vendor/index_pattern_management/public/components/field_editor/field_editor.less new file mode 100644 index 00000000..a181e5c4 --- /dev/null +++ b/web/src/components/vendor/index_pattern_management/public/components/field_editor/field_editor.less @@ -0,0 +1,8 @@ +.editor { + background: #fff; + :global { + .euiBadge__content { + font-weight: 400; + } + } +} \ No newline at end of file diff --git a/web/src/components/vendor/index_pattern_management/public/components/field_editor/field_editor.tsx b/web/src/components/vendor/index_pattern_management/public/components/field_editor/field_editor.tsx index 7de61d5f..9b374966 100644 --- a/web/src/components/vendor/index_pattern_management/public/components/field_editor/field_editor.tsx +++ b/web/src/components/vendor/index_pattern_management/public/components/field_editor/field_editor.tsx @@ -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 { + 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 ( + <> + + { + this.onMetricSettingsChange('name', e.target.value) + }} + /> + + + { + this.onMetricSettingsChange('option_aggs', value) + }} + spec={spec} + /> + + + + { + this.onMetricSettingsChange('unit', e.target.value) + }} + /> + + + { + this.onMetricSettingsChange('tags', value) + }}/> + + + ); + } + render() { const { isReady, isCreating, spec } = this.state; return isReady ? ( -
+

{isCreating ? ( @@ -774,6 +864,7 @@ export class FieldEditor extends PureComponent @@ -782,3 +873,140 @@ export class FieldEditor extends PureComponent { + + 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 ( + ({ 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 ( + + {value.map((tag, index) => ( + + handleRemove(index)} + style={{ height: '40px', lineHeight: '40px', fontSize: 14}} + > + {tag} + + + + ))} + {inputVisible && ( + + { + setInputVisible(false); + setInputValue(""); + }} + autoFocus + /> + + )} + {!inputVisible && ( + + + Add New + + + )} + + ); +}; diff --git a/web/src/components/vendor/index_pattern_management/public/components/field_editor/functions/index.jsx b/web/src/components/vendor/index_pattern_management/public/components/field_editor/functions/index.jsx new file mode 100644 index 00000000..f226ed7f --- /dev/null +++ b/web/src/components/vendor/index_pattern_management/public/components/field_editor/functions/index.jsx @@ -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 \ No newline at end of file diff --git a/web/src/components/vendor/index_pattern_management/public/components/field_editor/functions/latency.jsx b/web/src/components/vendor/index_pattern_management/public/components/field_editor/functions/latency.jsx new file mode 100644 index 00000000..35d28f39 --- /dev/null +++ b/web/src/components/vendor/index_pattern_management/public/components/field_editor/functions/latency.jsx @@ -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 ( + <> + + !!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} + /> + + + !!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} + /> + + + ) +} \ No newline at end of file diff --git a/web/src/components/vendor/index_pattern_management/public/components/field_editor/functions/latency_sum_func_value_in_group.jsx b/web/src/components/vendor/index_pattern_management/public/components/field_editor/functions/latency_sum_func_value_in_group.jsx new file mode 100644 index 00000000..ec071073 --- /dev/null +++ b/web/src/components/vendor/index_pattern_management/public/components/field_editor/functions/latency_sum_func_value_in_group.jsx @@ -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 ( + <> + + !!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} + /> + + + !!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} + /> + + + !!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} + /> + + {/* + ( + { 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} + /> + */} + + ) +} \ No newline at end of file diff --git a/web/src/components/vendor/index_pattern_management/public/components/field_editor/functions/rate.jsx b/web/src/components/vendor/index_pattern_management/public/components/field_editor/functions/rate.jsx new file mode 100644 index 00000000..6a3c349e --- /dev/null +++ b/web/src/components/vendor/index_pattern_management/public/components/field_editor/functions/rate.jsx @@ -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 ( + + !!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} + /> + + ) +} \ No newline at end of file diff --git a/web/src/components/vendor/index_pattern_management/public/components/field_editor/functions/rate_sum_func_value_in_group.jsx b/web/src/components/vendor/index_pattern_management/public/components/field_editor/functions/rate_sum_func_value_in_group.jsx new file mode 100644 index 00000000..67781352 --- /dev/null +++ b/web/src/components/vendor/index_pattern_management/public/components/field_editor/functions/rate_sum_func_value_in_group.jsx @@ -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 ( + <> + + !!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} + /> + + + !!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} + /> + + {/* + ( + { 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} + /> + */} + + ) +} \ No newline at end of file diff --git a/web/src/components/vendor/index_pattern_management/public/components/field_editor/functions/sum_func_value_in_group.jsx b/web/src/components/vendor/index_pattern_management/public/components/field_editor/functions/sum_func_value_in_group.jsx new file mode 100644 index 00000000..b287a448 --- /dev/null +++ b/web/src/components/vendor/index_pattern_management/public/components/field_editor/functions/sum_func_value_in_group.jsx @@ -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 ( + <> + + !!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} + /> + + + !!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} + /> + + {/* + ( + { 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} + /> + */} + + ) +} \ No newline at end of file diff --git a/web/src/locales/en-US.js b/web/src/locales/en-US.js index 8bde6bbe..a50e799d 100644 --- a/web/src/locales/en-US.js +++ b/web/src/locales/en-US.js @@ -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", diff --git a/web/src/locales/en-US/cluster.js b/web/src/locales/en-US/cluster.js index 48c75688..41173891 100644 --- a/web/src/locales/en-US/cluster.js +++ b/web/src/locales/en-US/cluster.js @@ -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", diff --git a/web/src/locales/zh-CN.js b/web/src/locales/zh-CN.js index 895cb22b..aa22f362 100644 --- a/web/src/locales/zh-CN.js +++ b/web/src/locales/zh-CN.js @@ -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": "添加", diff --git a/web/src/locales/zh-CN/cluster.js b/web/src/locales/zh-CN/cluster.js index ef0dae6d..63865abb 100644 --- a/web/src/locales/zh-CN/cluster.js +++ b/web/src/locales/zh-CN/cluster.js @@ -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": "索引延迟", diff --git a/web/src/pages/DataManagement/Discover.jsx b/web/src/pages/DataManagement/Discover.jsx index 9d1c0d9e..a73cdab4 100644 --- a/web/src/pages/DataManagement/Discover.jsx +++ b/web/src/pages/DataManagement/Discover.jsx @@ -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 (
@@ -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(); + // } + // }} />
@@ -1200,20 +1200,22 @@ const Discover = (props) => { getFilters={getSearchFilters} getBucketSize={getBucketSize} fullScreenHandle={fullScreenHandle} - layout={layout} + // layout={layout} selectedQueries={selectedQueries} /> - ) : mode === "layout" ? ( - - ) : ( + ) : + // mode === "layout" ? ( + // + // ) : + ( <> {indexPattern && ( diff --git a/web/src/pages/DataManagement/IndexPatterns.jsx b/web/src/pages/DataManagement/IndexPatterns.jsx index 58ed3a2e..bd16c017 100644 --- a/web/src/pages/DataManagement/IndexPatterns.jsx +++ b/web/src/pages/DataManagement/IndexPatterns.jsx @@ -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) => { - + { > - { - !isAgent && ( - - - - ) - } + + + { isAgent && ( { + 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 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 ( +
+ { data.length === 0 || data.some((item) => !Number.isFinite(item.value)) ? ( +
+ +
+ ) : ( + config?.sourceArea?.name ? ( + + ) : ( + + ) + )} +
+ ) +} \ No newline at end of file diff --git a/web/src/pages/Platform/Overview/components/TopN/Heatmap.jsx b/web/src/pages/Platform/Overview/components/TopN/Heatmap.jsx new file mode 100644 index 00000000..6f2155ae --- /dev/null +++ b/web/src/pages/Platform/Overview/components/TopN/Heatmap.jsx @@ -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 ( +
+
+ { + 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: + }) + return ( +
+ { +
+ {displayName} +
+ } +
+ { + markers.map((item, index) => ( +
+ {item.marker} + + {item.name}: + + {item.unit ? `${item.value}${item.unit}` : item.value} + + +
+ )) + } +
+
+ ); + } else { + return null + } + }, + } + }} /> +
+
+
+ ) +} \ No newline at end of file diff --git a/web/src/pages/Platform/Overview/components/TopN/Table.jsx b/web/src/pages/Platform/Overview/components/TopN/Table.jsx index 62ebf88e..838c28ed 100644 --- a/web/src/pages/Platform/Overview/components/TopN/Table.jsx +++ b/web/src/pages/Platform/Overview/components/TopN/Table.jsx @@ -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', diff --git a/web/src/pages/Platform/Overview/components/TopN/Treemap.jsx b/web/src/pages/Platform/Overview/components/TopN/Treemap.jsx index afe69792..ee52f766 100644 --- a/web/src/pages/Platform/Overview/components/TopN/Treemap.jsx +++ b/web/src/pages/Platform/Overview/components/TopN/Treemap.jsx @@ -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 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 (
- { data.length === 0 || data.some((item) => !Number.isFinite(item.value)) ? ( -
- -
- ) : ( - { 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) => { }, } }} /> - )}
) } \ No newline at end of file diff --git a/web/src/pages/Platform/Overview/components/TopN/index.jsx b/web/src/pages/Platform/Overview/components/TopN/index.jsx index 311986a6..f6ce7c5c 100644 --- a/web/src/pages/Platform/Overview/components/TopN/index.jsx +++ b/web/src/pages/Platform/Overview/components/TopN/index.jsx @@ -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) => { /> - +
+ Top +
{ precision={0} onChange={(value) => onFormDataChange({ top: value })} /> - +
+ {formatMessage({ id: "cluster.monitor.topn.area" })} +
- - +
+ {formatMessage({ id: "cluster.monitor.topn.color" })} +
- +
+ {formatMessage({ id: "cluster.monitor.topn.theme" })} +
{ onFormDataChange({ colors: value }) setConfig({ @@ -398,7 +574,7 @@ export default (props) => { colors: value }) }}/> - +

@@ -414,7 +590,7 @@ export default (props) => { ) } - { isTreemap ? :
} + { isTreemap ? :
} diff --git a/web/src/pages/Platform/Overview/components/TopN/index.less b/web/src/pages/Platform/Overview/components/TopN/index.less index f00b1aa7..3ea68b6c 100644 --- a/web/src/pages/Platform/Overview/components/TopN/index.less +++ b/web/src/pages/Platform/Overview/components/TopN/index.less @@ -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 { diff --git a/web/src/utils/authority.js b/web/src/utils/authority.js index 29dfe49c..73f07abd 100644 --- a/web/src/utils/authority.js +++ b/web/src/utils/authority.js @@ -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) } })(); diff --git a/web/src/utils/utils.js b/web/src/utils/utils.js index ed0ec208..e7cf8dbd 100644 --- a/web/src/utils/utils.js +++ b/web/src/utils/utils.js @@ -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); +}