[Metrics UI] Allow users to configure Inventory View palettes (#66948)
This commit is contained in:
parent
cd9084d439
commit
78db3eaffe
|
@ -50,6 +50,19 @@ export const inventoryViewSavedObjectType: SavedObjectsType = {
|
|||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
properties: {
|
||||
palette: {
|
||||
type: 'keyword',
|
||||
},
|
||||
steps: {
|
||||
type: 'long',
|
||||
},
|
||||
reverseColors: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
},
|
||||
groupBy: {
|
||||
type: 'nested',
|
||||
properties: {
|
||||
|
|
|
@ -10,6 +10,8 @@ import ApolloClient from 'apollo-client';
|
|||
import { AxiosRequestConfig } from 'axios';
|
||||
import React from 'react';
|
||||
import { Observable } from 'rxjs';
|
||||
import * as rt from 'io-ts';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SourceQuery } from '../graphql/types';
|
||||
import {
|
||||
SnapshotMetricInput,
|
||||
|
@ -125,29 +127,76 @@ export interface InfraWaffleMapGroupOfNodes extends InfraWaffleMapGroupBase {
|
|||
nodes: InfraWaffleMapNode[];
|
||||
}
|
||||
|
||||
export interface InfraWaffleMapStepRule {
|
||||
value: number;
|
||||
operator: InfraWaffleMapRuleOperator;
|
||||
color: string;
|
||||
label?: string;
|
||||
}
|
||||
export const OperatorRT = rt.keyof({
|
||||
gt: null,
|
||||
gte: null,
|
||||
lt: null,
|
||||
lte: null,
|
||||
eq: null,
|
||||
});
|
||||
|
||||
export interface InfraWaffleMapGradientRule {
|
||||
value: number;
|
||||
color: string;
|
||||
}
|
||||
export const PALETTES = {
|
||||
status: i18n.translate('xpack.infra.legendControls.palettes.status', {
|
||||
defaultMessage: 'Status',
|
||||
}),
|
||||
temperature: i18n.translate('xpack.infra.legendControls.palettes.temperature', {
|
||||
defaultMessage: 'Temperature',
|
||||
}),
|
||||
cool: i18n.translate('xpack.infra.legendControls.palettes.cool', {
|
||||
defaultMessage: 'Cool',
|
||||
}),
|
||||
warm: i18n.translate('xpack.infra.legendControls.palettes.warm', {
|
||||
defaultMessage: 'Warm',
|
||||
}),
|
||||
positive: i18n.translate('xpack.infra.legendControls.palettes.positive', {
|
||||
defaultMessage: 'Positive',
|
||||
}),
|
||||
negative: i18n.translate('xpack.infra.legendControls.palettes.negative', {
|
||||
defaultMessage: 'Negative',
|
||||
}),
|
||||
};
|
||||
|
||||
export interface InfraWaffleMapStepLegend {
|
||||
type: 'step';
|
||||
rules: InfraWaffleMapStepRule[];
|
||||
}
|
||||
export const InventoryColorPaletteRT = rt.keyof(PALETTES);
|
||||
export type InventoryColorPalette = rt.TypeOf<typeof InventoryColorPaletteRT>;
|
||||
|
||||
export interface InfraWaffleMapGradientLegend {
|
||||
type: 'gradient';
|
||||
rules: InfraWaffleMapGradientRule[];
|
||||
}
|
||||
export const StepRuleRT = rt.intersection([
|
||||
rt.type({
|
||||
value: rt.number,
|
||||
operator: OperatorRT,
|
||||
color: rt.string,
|
||||
}),
|
||||
rt.partial({ label: rt.string }),
|
||||
]);
|
||||
|
||||
export type InfraWaffleMapLegend = InfraWaffleMapStepLegend | InfraWaffleMapGradientLegend;
|
||||
export const StepLegendRT = rt.type({
|
||||
type: rt.literal('step'),
|
||||
rules: rt.array(StepRuleRT),
|
||||
});
|
||||
export type InfraWaffleMapStepRule = rt.TypeOf<typeof StepRuleRT>;
|
||||
export type InfraWaffleMapStepLegend = rt.TypeOf<typeof StepLegendRT>;
|
||||
|
||||
export const GradientRuleRT = rt.type({
|
||||
value: rt.number,
|
||||
color: rt.string,
|
||||
});
|
||||
|
||||
export const GradientLegendRT = rt.type({
|
||||
type: rt.literal('gradient'),
|
||||
rules: rt.array(GradientRuleRT),
|
||||
});
|
||||
|
||||
export type InfraWaffleMapGradientRule = rt.TypeOf<typeof GradientRuleRT>;
|
||||
export type InfraWaffleMapGradientLegend = rt.TypeOf<typeof GradientLegendRT>;
|
||||
|
||||
export const SteppedGradientLegendRT = rt.type({
|
||||
type: rt.literal('steppedGradient'),
|
||||
rules: rt.array(GradientRuleRT),
|
||||
});
|
||||
|
||||
export type InfraWaffleMapSteppedGradientLegend = rt.TypeOf<typeof SteppedGradientLegendRT>;
|
||||
|
||||
export const LegendRT = rt.union([StepLegendRT, GradientLegendRT, SteppedGradientLegendRT]);
|
||||
export type InfraWaffleMapLegend = rt.TypeOf<typeof LegendRT>;
|
||||
|
||||
export enum InfraWaffleMapRuleOperator {
|
||||
gt = 'gt',
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { useInterval } from 'react-use';
|
||||
|
||||
import { euiPaletteColorBlind, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { convertIntervalToString } from '../../../../utils/convert_interval_to_string';
|
||||
import { NodesOverview, calculateBoundsFromNodes } from './nodes_overview';
|
||||
import { PageContent } from '../../../../components/page';
|
||||
|
@ -16,7 +16,7 @@ import { useWaffleTimeContext } from '../hooks/use_waffle_time';
|
|||
import { useWaffleFiltersContext } from '../hooks/use_waffle_filters';
|
||||
import { useWaffleOptionsContext } from '../hooks/use_waffle_options';
|
||||
import { useSourceContext } from '../../../../containers/source';
|
||||
import { InfraFormatterType, InfraWaffleMapGradientLegend } from '../../../../lib/lib';
|
||||
import { InfraFormatterType } from '../../../../lib/lib';
|
||||
import { euiStyled } from '../../../../../../observability/public';
|
||||
import { Toolbar } from './toolbars/toolbar';
|
||||
import { ViewSwitcher } from './waffle/view_switcher';
|
||||
|
@ -24,8 +24,7 @@ import { SavedViews } from './saved_views';
|
|||
import { IntervalLabel } from './waffle/interval_label';
|
||||
import { Legend } from './waffle/legend';
|
||||
import { createInventoryMetricFormatter } from '../lib/create_inventory_metric_formatter';
|
||||
|
||||
const euiVisColorPalette = euiPaletteColorBlind();
|
||||
import { createLegend } from '../lib/create_legend';
|
||||
|
||||
export const Layout = () => {
|
||||
const { sourceId, source } = useSourceContext();
|
||||
|
@ -40,6 +39,7 @@ export const Layout = () => {
|
|||
view,
|
||||
autoBounds,
|
||||
boundsOverride,
|
||||
legend,
|
||||
} = useWaffleOptionsContext();
|
||||
const { currentTime, jumpToTime, isAutoReloading } = useWaffleTimeContext();
|
||||
const { filterQueryAsJson, applyFilterQuery } = useWaffleFiltersContext();
|
||||
|
@ -57,13 +57,7 @@ export const Layout = () => {
|
|||
const options = {
|
||||
formatter: InfraFormatterType.percent,
|
||||
formatTemplate: '{{value}}',
|
||||
legend: {
|
||||
type: 'gradient',
|
||||
rules: [
|
||||
{ value: 0, color: '#D3DAE6' },
|
||||
{ value: 1, color: euiVisColorPalette[1] },
|
||||
],
|
||||
} as InfraWaffleMapGradientLegend,
|
||||
legend: createLegend(legend.palette, legend.steps, legend.reverseColors),
|
||||
metric,
|
||||
sort,
|
||||
fields: source?.configuration?.fields,
|
||||
|
|
|
@ -33,6 +33,7 @@ export const ToolbarWrapper = (props: Props) => {
|
|||
accountId,
|
||||
view,
|
||||
region,
|
||||
legend,
|
||||
sort,
|
||||
customMetrics,
|
||||
changeCustomMetrics,
|
||||
|
@ -59,6 +60,7 @@ export const ToolbarWrapper = (props: Props) => {
|
|||
nodeType,
|
||||
region,
|
||||
accountId,
|
||||
legend,
|
||||
customMetrics,
|
||||
changeCustomMetrics,
|
||||
})}
|
||||
|
|
|
@ -3,15 +3,22 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { euiStyled } from '../../../../../../../observability/public';
|
||||
import { InfraFormatter, InfraWaffleMapBounds, InfraWaffleMapLegend } from '../../../../../lib/lib';
|
||||
import {
|
||||
InfraFormatter,
|
||||
InfraWaffleMapBounds,
|
||||
InfraWaffleMapLegend,
|
||||
SteppedGradientLegendRT,
|
||||
StepLegendRT,
|
||||
GradientLegendRT,
|
||||
} from '../../../../../lib/lib';
|
||||
import { GradientLegend } from './gradient_legend';
|
||||
import { LegendControls } from './legend_controls';
|
||||
import { isInfraWaffleMapGradientLegend, isInfraWaffleMapStepLegend } from '../../lib/type_guards';
|
||||
import { StepLegend } from './steps_legend';
|
||||
import { useWaffleOptionsContext } from '../../hooks/use_waffle_options';
|
||||
import { useWaffleOptionsContext, WaffleLegendOptions } from '../../hooks/use_waffle_options';
|
||||
import { SteppedGradientLegend } from './stepped_gradient_legend';
|
||||
interface Props {
|
||||
legend: InfraWaffleMapLegend;
|
||||
bounds: InfraWaffleMapBounds;
|
||||
|
@ -22,6 +29,7 @@ interface Props {
|
|||
interface LegendControlOptions {
|
||||
auto: boolean;
|
||||
bounds: InfraWaffleMapBounds;
|
||||
legend: WaffleLegendOptions;
|
||||
}
|
||||
|
||||
export const Legend: React.FC<Props> = ({ dataBounds, legend, bounds, formatter }) => {
|
||||
|
@ -29,24 +37,35 @@ export const Legend: React.FC<Props> = ({ dataBounds, legend, bounds, formatter
|
|||
changeBoundsOverride,
|
||||
changeAutoBounds,
|
||||
autoBounds,
|
||||
legend: legendOptions,
|
||||
changeLegend,
|
||||
boundsOverride,
|
||||
} = useWaffleOptionsContext();
|
||||
const handleChange = useCallback(
|
||||
(options: LegendControlOptions) => {
|
||||
changeBoundsOverride(options.bounds);
|
||||
changeAutoBounds(options.auto);
|
||||
changeLegend(options.legend);
|
||||
},
|
||||
[changeBoundsOverride, changeAutoBounds, changeLegend]
|
||||
);
|
||||
return (
|
||||
<LegendContainer>
|
||||
<LegendControls
|
||||
options={legendOptions}
|
||||
dataBounds={dataBounds}
|
||||
bounds={bounds}
|
||||
autoBounds={autoBounds}
|
||||
boundsOverride={boundsOverride}
|
||||
onChange={(options: LegendControlOptions) => {
|
||||
changeBoundsOverride(options.bounds);
|
||||
changeAutoBounds(options.auto);
|
||||
}}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{isInfraWaffleMapGradientLegend(legend) && (
|
||||
{GradientLegendRT.is(legend) && (
|
||||
<GradientLegend formatter={formatter} legend={legend} bounds={bounds} />
|
||||
)}
|
||||
{isInfraWaffleMapStepLegend(legend) && <StepLegend formatter={formatter} legend={legend} />}
|
||||
{StepLegendRT.is(legend) && <StepLegend formatter={formatter} legend={legend} />}
|
||||
{SteppedGradientLegendRT.is(legend) && (
|
||||
<SteppedGradientLegend formatter={formatter} bounds={bounds} legend={legend} />
|
||||
)}
|
||||
</LegendContainer>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,11 +5,10 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
EuiFieldNumber,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiPopover,
|
||||
|
@ -17,27 +16,65 @@ import {
|
|||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiSwitchEvent,
|
||||
EuiText,
|
||||
EuiSelect,
|
||||
EuiRange,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { SyntheticEvent, useState } from 'react';
|
||||
|
||||
import React, { SyntheticEvent, useState, useCallback, useEffect } from 'react';
|
||||
import { first, last } from 'lodash';
|
||||
import { euiStyled } from '../../../../../../../observability/public';
|
||||
import { InfraWaffleMapBounds } from '../../../../../lib/lib';
|
||||
import { InfraWaffleMapBounds, InventoryColorPalette, PALETTES } from '../../../../../lib/lib';
|
||||
import { WaffleLegendOptions } from '../../hooks/use_waffle_options';
|
||||
import { getColorPalette } from '../../lib/get_color_palette';
|
||||
import { convertBoundsToPercents } from '../../lib/convert_bounds_to_percents';
|
||||
import { SwatchLabel } from './swatch_label';
|
||||
import { PalettePreview } from './palette_preview';
|
||||
|
||||
interface Props {
|
||||
onChange: (options: { auto: boolean; bounds: InfraWaffleMapBounds }) => void;
|
||||
onChange: (options: {
|
||||
auto: boolean;
|
||||
bounds: InfraWaffleMapBounds;
|
||||
legend: WaffleLegendOptions;
|
||||
}) => void;
|
||||
bounds: InfraWaffleMapBounds;
|
||||
dataBounds: InfraWaffleMapBounds;
|
||||
autoBounds: boolean;
|
||||
boundsOverride: InfraWaffleMapBounds;
|
||||
options: WaffleLegendOptions;
|
||||
}
|
||||
|
||||
export const LegendControls = ({ autoBounds, boundsOverride, onChange, dataBounds }: Props) => {
|
||||
const PALETTE_NAMES: InventoryColorPalette[] = [
|
||||
'temperature',
|
||||
'status',
|
||||
'cool',
|
||||
'warm',
|
||||
'positive',
|
||||
'negative',
|
||||
];
|
||||
|
||||
const PALETTE_OPTIONS = PALETTE_NAMES.map(name => ({ text: PALETTES[name], value: name }));
|
||||
|
||||
export const LegendControls = ({
|
||||
autoBounds,
|
||||
boundsOverride,
|
||||
onChange,
|
||||
dataBounds,
|
||||
options,
|
||||
}: Props) => {
|
||||
const [isPopoverOpen, setPopoverState] = useState(false);
|
||||
const [draftAuto, setDraftAuto] = useState(autoBounds);
|
||||
const [draftBounds, setDraftBounds] = useState(autoBounds ? dataBounds : boundsOverride); // should come from bounds prop
|
||||
const [draftLegend, setLegendOptions] = useState(options);
|
||||
const [draftBounds, setDraftBounds] = useState(convertBoundsToPercents(boundsOverride)); // should come from bounds prop
|
||||
|
||||
useEffect(() => {
|
||||
if (draftAuto) {
|
||||
setDraftBounds(convertBoundsToPercents(dataBounds));
|
||||
}
|
||||
}, [autoBounds, dataBounds, draftAuto, onChange, options]);
|
||||
|
||||
const buttonComponent = (
|
||||
<EuiButtonIcon
|
||||
iconType="controlsHorizontal"
|
||||
|
@ -49,108 +86,259 @@ export const LegendControls = ({ autoBounds, boundsOverride, onChange, dataBound
|
|||
/>
|
||||
);
|
||||
|
||||
const handleAutoChange = (e: EuiSwitchEvent) => {
|
||||
setDraftAuto(e.target.checked);
|
||||
};
|
||||
const handleAutoChange = useCallback(
|
||||
(e: EuiSwitchEvent) => {
|
||||
const auto = e.target.checked;
|
||||
setDraftAuto(auto);
|
||||
if (!auto) {
|
||||
setDraftBounds(convertBoundsToPercents(boundsOverride));
|
||||
}
|
||||
},
|
||||
[boundsOverride]
|
||||
);
|
||||
|
||||
const createBoundsHandler = (name: string) => (e: SyntheticEvent<HTMLInputElement>) => {
|
||||
const value = parseFloat(e.currentTarget.value);
|
||||
setDraftBounds({ ...draftBounds, [name]: value });
|
||||
};
|
||||
const handleReverseColors = useCallback(
|
||||
(e: EuiSwitchEvent) => {
|
||||
setLegendOptions(previous => ({ ...previous, reverseColors: e.target.checked }));
|
||||
},
|
||||
[setLegendOptions]
|
||||
);
|
||||
|
||||
const handlePopoverClose = () => {
|
||||
const handleMaxBounds = useCallback(
|
||||
(e: SyntheticEvent<HTMLInputElement>) => {
|
||||
const value = parseFloat(e.currentTarget.value);
|
||||
// Auto correct the max to be one larger then the min OR 100
|
||||
const max = value <= draftBounds.min ? draftBounds.min + 1 : value > 100 ? 100 : value;
|
||||
setDraftBounds({ ...draftBounds, max });
|
||||
},
|
||||
[draftBounds]
|
||||
);
|
||||
|
||||
const handleMinBounds = useCallback(
|
||||
(e: SyntheticEvent<HTMLInputElement>) => {
|
||||
const value = parseFloat(e.currentTarget.value);
|
||||
// Auto correct the min to be one smaller then the max OR ZERO
|
||||
const min = value >= draftBounds.max ? draftBounds.max - 1 : value < 0 ? 0 : value;
|
||||
setDraftBounds({ ...draftBounds, min });
|
||||
},
|
||||
[draftBounds]
|
||||
);
|
||||
|
||||
const handleApplyClick = useCallback(() => {
|
||||
onChange({
|
||||
auto: draftAuto,
|
||||
bounds: { min: draftBounds.min / 100, max: draftBounds.max / 100 },
|
||||
legend: draftLegend,
|
||||
});
|
||||
}, [onChange, draftAuto, draftBounds, draftLegend]);
|
||||
|
||||
const handleCancelClick = useCallback(() => {
|
||||
setDraftBounds(convertBoundsToPercents(boundsOverride));
|
||||
setDraftAuto(autoBounds);
|
||||
setLegendOptions(options);
|
||||
setPopoverState(false);
|
||||
};
|
||||
}, [autoBounds, boundsOverride, options]);
|
||||
|
||||
const handleApplyClick = () => {
|
||||
onChange({ auto: draftAuto, bounds: draftBounds });
|
||||
};
|
||||
const handleStepsChange = useCallback(
|
||||
e => {
|
||||
setLegendOptions(previous => ({ ...previous, steps: parseInt(e.target.value, 10) }));
|
||||
},
|
||||
[setLegendOptions]
|
||||
);
|
||||
|
||||
const handlePaletteChange = useCallback(
|
||||
e => {
|
||||
setLegendOptions(previous => ({ ...previous, palette: e.target.value }));
|
||||
},
|
||||
[setLegendOptions]
|
||||
);
|
||||
|
||||
const commited =
|
||||
draftAuto === autoBounds &&
|
||||
boundsOverride.min === draftBounds.min &&
|
||||
boundsOverride.max === draftBounds.max;
|
||||
boundsOverride.min * 100 === draftBounds.min &&
|
||||
boundsOverride.max * 100 === draftBounds.max &&
|
||||
options.steps === draftLegend.steps &&
|
||||
options.reverseColors === draftLegend.reverseColors &&
|
||||
options.palette === draftLegend.palette;
|
||||
|
||||
const boundsValidRange = draftBounds.min < draftBounds.max;
|
||||
const paletteColors = getColorPalette(
|
||||
draftLegend.palette,
|
||||
draftLegend.steps,
|
||||
draftLegend.reverseColors
|
||||
);
|
||||
const errors = !boundsValidRange
|
||||
? [
|
||||
i18n.translate('xpack.infra.legnedControls.boundRangeError', {
|
||||
defaultMessage: 'Minimum must be smaller then the maximum',
|
||||
}),
|
||||
]
|
||||
: [];
|
||||
|
||||
return (
|
||||
<ControlContainer>
|
||||
<EuiPopover
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={handlePopoverClose}
|
||||
closePopover={handleCancelClick}
|
||||
id="legendControls"
|
||||
button={buttonComponent}
|
||||
withTitle
|
||||
>
|
||||
<EuiPopoverTitle>Legend Options</EuiPopoverTitle>
|
||||
<EuiForm>
|
||||
<EuiFormRow>
|
||||
<EuiSwitch
|
||||
name="bounds"
|
||||
label={i18n.translate('xpack.infra.legendControls.switchLabel', {
|
||||
defaultMessage: 'Auto calculate range',
|
||||
})}
|
||||
checked={draftAuto}
|
||||
onChange={handleAutoChange}
|
||||
<EuiForm style={{ width: 500 }}>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
display="columnCompressed"
|
||||
label={i18n.translate('xpack.infra.legendControls.colorPaletteLabel', {
|
||||
defaultMessage: 'Color palette',
|
||||
})}
|
||||
>
|
||||
<>
|
||||
<EuiSelect
|
||||
options={PALETTE_OPTIONS}
|
||||
value={draftLegend.palette}
|
||||
id="palette"
|
||||
onChange={handlePaletteChange}
|
||||
compressed
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<PalettePreview
|
||||
palette={draftLegend.palette}
|
||||
steps={draftLegend.steps}
|
||||
reverse={draftLegend.reverseColors}
|
||||
/>
|
||||
</>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
display="columnCompressed"
|
||||
label={i18n.translate('xpack.infra.legendControls.stepsLabel', {
|
||||
defaultMessage: 'Number of colors',
|
||||
})}
|
||||
>
|
||||
<EuiRange
|
||||
id="steps"
|
||||
min={2}
|
||||
max={20}
|
||||
step={1}
|
||||
value={draftLegend.steps}
|
||||
onChange={handleStepsChange}
|
||||
showValue
|
||||
compressed
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer />
|
||||
{(!boundsValidRange && (
|
||||
<EuiText color="danger" grow={false} size="s">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.legendControls.errorMessage"
|
||||
defaultMessage="Min should be less than max"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
)) ||
|
||||
null}
|
||||
<EuiFlexGroup style={{ marginTop: 0 }}>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
display="columnCompressed"
|
||||
label={i18n.translate('xpack.infra.legendControls.reverseDirectionLabel', {
|
||||
defaultMessage: 'Reverse direction',
|
||||
})}
|
||||
>
|
||||
<EuiSwitch
|
||||
showLabel={false}
|
||||
name="reverseColors"
|
||||
label="reverseColors"
|
||||
checked={draftLegend.reverseColors}
|
||||
onChange={handleReverseColors}
|
||||
compressed
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
display="columnCompressed"
|
||||
label={i18n.translate('xpack.infra.legendControls.switchLabel', {
|
||||
defaultMessage: 'Auto calculate range',
|
||||
})}
|
||||
>
|
||||
<EuiSwitch
|
||||
showLabel={false}
|
||||
name="bounds"
|
||||
label="bounds"
|
||||
checked={draftAuto}
|
||||
onChange={handleAutoChange}
|
||||
compressed
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={
|
||||
<SwatchLabel
|
||||
color={first(paletteColors)}
|
||||
label={i18n.translate('xpack.infra.legendControls.minLabel', {
|
||||
defaultMessage: 'Min',
|
||||
defaultMessage: 'Minimum',
|
||||
})}
|
||||
/>
|
||||
}
|
||||
isInvalid={!boundsValidRange}
|
||||
display="columnCompressed"
|
||||
error={errors}
|
||||
>
|
||||
<div style={{ maxWidth: 150 }}>
|
||||
<EuiFieldNumber
|
||||
disabled={draftAuto}
|
||||
step={1}
|
||||
value={isNaN(draftBounds.min) ? '' : draftBounds.min}
|
||||
isInvalid={!boundsValidRange}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
disabled={draftAuto}
|
||||
step={0.1}
|
||||
value={isNaN(draftBounds.min) ? '' : draftBounds.min}
|
||||
isInvalid={!boundsValidRange}
|
||||
name="legendMin"
|
||||
onChange={createBoundsHandler('min')}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
name="legendMin"
|
||||
onChange={handleMinBounds}
|
||||
append="%"
|
||||
compressed
|
||||
/>
|
||||
</div>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
display="columnCompressed"
|
||||
label={
|
||||
<SwatchLabel
|
||||
color={last(paletteColors)}
|
||||
label={i18n.translate('xpack.infra.legendControls.maxLabel', {
|
||||
defaultMessage: 'Max',
|
||||
defaultMessage: 'Maxium',
|
||||
})}
|
||||
/>
|
||||
}
|
||||
isInvalid={!boundsValidRange}
|
||||
error={errors}
|
||||
>
|
||||
<div style={{ maxWidth: 150 }}>
|
||||
<EuiFieldNumber
|
||||
disabled={draftAuto}
|
||||
step={1}
|
||||
isInvalid={!boundsValidRange}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
disabled={draftAuto}
|
||||
step={0.1}
|
||||
isInvalid={!boundsValidRange}
|
||||
value={isNaN(draftBounds.max) ? '' : draftBounds.max}
|
||||
name="legendMax"
|
||||
onChange={createBoundsHandler('max')}
|
||||
value={isNaN(draftBounds.max) ? '' : draftBounds.max}
|
||||
name="legendMax"
|
||||
onChange={handleMaxBounds}
|
||||
append="%"
|
||||
compressed
|
||||
/>
|
||||
</div>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty type="submit" size="s" onClick={handleCancelClick}>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.legendControls.cancelButton"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
type="submit"
|
||||
size="s"
|
||||
fill
|
||||
disabled={commited || !boundsValidRange}
|
||||
onClick={handleApplyClick}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.legendControls.applyButton"
|
||||
defaultMessage="Apply"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiButton
|
||||
type="submit"
|
||||
size="s"
|
||||
fill
|
||||
disabled={commited || !boundsValidRange}
|
||||
onClick={handleApplyClick}
|
||||
>
|
||||
<FormattedMessage id="xpack.infra.legendControls.applyButton" defaultMessage="Apply" />
|
||||
</EuiButton>
|
||||
</EuiForm>
|
||||
</EuiPopover>
|
||||
</ControlContainer>
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { euiStyled } from '../../../../../../../observability/public';
|
||||
import { InventoryColorPalette } from '../../../../../lib/lib';
|
||||
import { getColorPalette } from '../../lib/get_color_palette';
|
||||
|
||||
interface Props {
|
||||
palette: InventoryColorPalette;
|
||||
steps: number;
|
||||
reverse: boolean;
|
||||
}
|
||||
|
||||
export const PalettePreview = ({ steps, palette, reverse }: Props) => {
|
||||
const colors = getColorPalette(palette, steps, reverse);
|
||||
return (
|
||||
<Swatches>
|
||||
{colors.map(color => (
|
||||
<Swatch key={color} style={{ backgroundColor: color }} />
|
||||
))}
|
||||
</Swatches>
|
||||
);
|
||||
};
|
||||
|
||||
const Swatch = euiStyled.div`
|
||||
width: 16px;
|
||||
height: 12px;
|
||||
flex: 0 0 auto;
|
||||
&:first-child {
|
||||
border-radius: ${props => props.theme.eui.euiBorderRadius} 0 0 ${props =>
|
||||
props.theme.eui.euiBorderRadius};
|
||||
}
|
||||
&:last-child {
|
||||
border-radius: 0 ${props => props.theme.eui.euiBorderRadius} ${props =>
|
||||
props.theme.eui.euiBorderRadius} 0;
|
||||
`;
|
||||
|
||||
const Swatches = euiStyled.div`
|
||||
display: flex;
|
||||
`;
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { euiStyled } from '../../../../../../../observability/public';
|
||||
import {
|
||||
InfraWaffleMapBounds,
|
||||
InfraFormatter,
|
||||
InfraWaffleMapSteppedGradientLegend,
|
||||
} from '../../../../../lib/lib';
|
||||
|
||||
interface Props {
|
||||
legend: InfraWaffleMapSteppedGradientLegend;
|
||||
bounds: InfraWaffleMapBounds;
|
||||
formatter: InfraFormatter;
|
||||
}
|
||||
|
||||
export const SteppedGradientLegend: React.FC<Props> = ({ legend, bounds, formatter }) => {
|
||||
return (
|
||||
<LegendContainer>
|
||||
<Ticks>
|
||||
<TickLabel value={0} bounds={bounds} formatter={formatter} />
|
||||
<TickLabel value={0.5} bounds={bounds} formatter={formatter} />
|
||||
<TickLabel value={1} bounds={bounds} formatter={formatter} />
|
||||
</Ticks>
|
||||
<GradientContainer>
|
||||
{legend.rules.map((rule, index) => (
|
||||
<GradientStep
|
||||
key={`step-${index}-${rule.value}`}
|
||||
style={{ backgroundColor: rule.color }}
|
||||
/>
|
||||
))}
|
||||
</GradientContainer>
|
||||
</LegendContainer>
|
||||
);
|
||||
};
|
||||
|
||||
interface TickProps {
|
||||
bounds: InfraWaffleMapBounds;
|
||||
value: number;
|
||||
formatter: InfraFormatter;
|
||||
}
|
||||
|
||||
const TickLabel = ({ value, bounds, formatter }: TickProps) => {
|
||||
const normalizedValue = value === 0 ? bounds.min : bounds.max * value;
|
||||
const style = { left: `${value * 100}%` };
|
||||
const label = formatter(normalizedValue);
|
||||
return <Tick style={style}>{label}</Tick>;
|
||||
};
|
||||
|
||||
const GradientStep = euiStyled.div`
|
||||
height: ${props => props.theme.eui.paddingSizes.s};
|
||||
flex: 1 1 auto;
|
||||
&:first-child {
|
||||
border-radius: ${props => props.theme.eui.euiBorderRadius} 0 0 ${props =>
|
||||
props.theme.eui.euiBorderRadius};
|
||||
}
|
||||
&:last-child {
|
||||
border-radius: 0 ${props => props.theme.eui.euiBorderRadius} ${props =>
|
||||
props.theme.eui.euiBorderRadius} 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const Ticks = euiStyled.div`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
top: -18px;
|
||||
`;
|
||||
|
||||
const Tick = euiStyled.div`
|
||||
position: absolute;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
top: 0;
|
||||
left: 0;
|
||||
white-space: nowrap;
|
||||
transform: translate(-50%, 0);
|
||||
&:first-child {
|
||||
padding-left: 5px;
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
&:last-child {
|
||||
padding-right: 5px;
|
||||
transform: translate(-100%, 0);
|
||||
}
|
||||
`;
|
||||
|
||||
const GradientContainer = euiStyled.div`
|
||||
display: flex;
|
||||
flex-direction; row;
|
||||
align-items: stretch;
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
const LegendContainer = euiStyled.div`
|
||||
position: absolute;
|
||||
height: 10px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 40px;
|
||||
`;
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiColorPickerSwatch, EuiText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
export interface Props {
|
||||
color: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const SwatchLabel = ({ label, color }: Props) => {
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiColorPickerSwatch color={color} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="xs">
|
||||
<strong>{label}</strong>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -10,6 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable';
|
|||
import { fold } from 'fp-ts/lib/Either';
|
||||
import { constant, identity } from 'fp-ts/lib/function';
|
||||
import createContainer from 'constate';
|
||||
import { InventoryColorPaletteRT } from '../../../../lib/lib';
|
||||
import {
|
||||
SnapshotMetricInput,
|
||||
SnapshotGroupBy,
|
||||
|
@ -32,6 +33,11 @@ export const DEFAULT_WAFFLE_OPTIONS_STATE: WaffleOptionsState = {
|
|||
accountId: '',
|
||||
region: '',
|
||||
customMetrics: [],
|
||||
legend: {
|
||||
palette: 'cool',
|
||||
steps: 10,
|
||||
reverseColors: false,
|
||||
},
|
||||
sort: { by: 'name', direction: 'desc' },
|
||||
};
|
||||
|
||||
|
@ -100,6 +106,13 @@ export const useWaffleOptions = () => {
|
|||
[setState]
|
||||
);
|
||||
|
||||
const changeLegend = useCallback(
|
||||
(legend: WaffleLegendOptions) => {
|
||||
setState(previous => ({ ...previous, legend }));
|
||||
},
|
||||
[setState]
|
||||
);
|
||||
|
||||
const changeSort = useCallback(
|
||||
(sort: WaffleSortOption) => {
|
||||
setState(previous => ({ ...previous, sort }));
|
||||
|
@ -120,11 +133,20 @@ export const useWaffleOptions = () => {
|
|||
changeAccount,
|
||||
changeRegion,
|
||||
changeCustomMetrics,
|
||||
changeLegend,
|
||||
changeSort,
|
||||
setWaffleOptionsState: setState,
|
||||
};
|
||||
};
|
||||
|
||||
const WaffleLegendOptionsRT = rt.type({
|
||||
palette: InventoryColorPaletteRT,
|
||||
steps: rt.number,
|
||||
reverseColors: rt.boolean,
|
||||
});
|
||||
|
||||
export type WaffleLegendOptions = rt.TypeOf<typeof WaffleLegendOptionsRT>;
|
||||
|
||||
export const WaffleSortOptionRT = rt.type({
|
||||
by: rt.keyof({ name: null, value: null }),
|
||||
direction: rt.keyof({ asc: null, desc: null }),
|
||||
|
@ -149,6 +171,7 @@ export const WaffleOptionsStateRT = rt.type({
|
|||
accountId: rt.string,
|
||||
region: rt.string,
|
||||
customMetrics: rt.array(SnapshotCustomMetricInputRT),
|
||||
legend: WaffleLegendOptionsRT,
|
||||
sort: WaffleSortOptionRT,
|
||||
});
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ export const useWaffleViewState = () => {
|
|||
autoBounds,
|
||||
accountId,
|
||||
region,
|
||||
legend,
|
||||
sort,
|
||||
setWaffleOptionsState,
|
||||
} = useWaffleOptionsContext();
|
||||
|
@ -49,6 +50,7 @@ export const useWaffleViewState = () => {
|
|||
time: currentTime,
|
||||
autoReload: isAutoReloading,
|
||||
filterQuery,
|
||||
legend,
|
||||
};
|
||||
|
||||
const defaultViewState: WaffleViewState = {
|
||||
|
@ -72,6 +74,7 @@ export const useWaffleViewState = () => {
|
|||
autoBounds: newState.autoBounds,
|
||||
accountId: newState.accountId,
|
||||
region: newState.region,
|
||||
legend: newState.legend,
|
||||
});
|
||||
if (newState.time) {
|
||||
setWaffleTimeState({
|
||||
|
|
|
@ -8,12 +8,14 @@ import { eq, first, gt, gte, last, lt, lte, sortBy } from 'lodash';
|
|||
import { mix, parseToRgb, toColorString } from 'polished';
|
||||
import {
|
||||
InfraWaffleMapBounds,
|
||||
InfraWaffleMapGradientLegend,
|
||||
InfraWaffleMapLegend,
|
||||
InfraWaffleMapRuleOperator,
|
||||
InfraWaffleMapStepLegend,
|
||||
GradientLegendRT,
|
||||
SteppedGradientLegendRT,
|
||||
StepLegendRT,
|
||||
InfraWaffleMapStepRule,
|
||||
InfraWaffleMapGradientRule,
|
||||
} from '../../../../lib/lib';
|
||||
import { isInfraWaffleMapGradientLegend, isInfraWaffleMapStepLegend } from './type_guards';
|
||||
|
||||
const OPERATOR_TO_FN = {
|
||||
[InfraWaffleMapRuleOperator.eq]: eq,
|
||||
|
@ -34,11 +36,16 @@ export const colorFromValue = (
|
|||
defaultColor = 'rgba(217, 217, 217, 1)'
|
||||
): string => {
|
||||
try {
|
||||
if (isInfraWaffleMapStepLegend(legend)) {
|
||||
return convertToRgbString(calculateStepColor(legend, value, defaultColor));
|
||||
if (StepLegendRT.is(legend)) {
|
||||
return convertToRgbString(calculateStepColor(legend.rules, value, defaultColor));
|
||||
}
|
||||
if (isInfraWaffleMapGradientLegend(legend)) {
|
||||
return convertToRgbString(calculateGradientColor(legend, value, bounds, defaultColor));
|
||||
if (GradientLegendRT.is(legend)) {
|
||||
return convertToRgbString(calculateGradientColor(legend.rules, value, bounds, defaultColor));
|
||||
}
|
||||
if (SteppedGradientLegendRT.is(legend)) {
|
||||
return convertToRgbString(
|
||||
calculateSteppedGradientColor(legend.rules, value, bounds, defaultColor)
|
||||
);
|
||||
}
|
||||
return defaultColor;
|
||||
} catch (error) {
|
||||
|
@ -50,12 +57,37 @@ const normalizeValue = (min: number, max: number, value: number): number => {
|
|||
return (value - min) / (max - min);
|
||||
};
|
||||
|
||||
export const calculateSteppedGradientColor = (
|
||||
rules: InfraWaffleMapGradientRule[],
|
||||
value: number | string,
|
||||
bounds: InfraWaffleMapBounds,
|
||||
defaultColor = 'rgba(217, 217, 217, 1)'
|
||||
) => {
|
||||
const normalizedValue = normalizeValue(bounds.min, bounds.max, Number(value));
|
||||
const steps = rules.length;
|
||||
|
||||
// Since the stepped legend matches a range we need to ensure anything outside
|
||||
// the max bounds get's the maximum color.
|
||||
if (gte(normalizedValue, last(rules).value)) {
|
||||
return last(rules).color;
|
||||
}
|
||||
|
||||
return rules.reduce((color: string, rule) => {
|
||||
const min = rule.value - 1 / steps;
|
||||
const max = rule.value;
|
||||
if (gte(normalizedValue, min) && lte(normalizedValue, max)) {
|
||||
return rule.color;
|
||||
}
|
||||
return color;
|
||||
}, first(rules).color || defaultColor);
|
||||
};
|
||||
|
||||
export const calculateStepColor = (
|
||||
legend: InfraWaffleMapStepLegend,
|
||||
rules: InfraWaffleMapStepRule[],
|
||||
value: number | string,
|
||||
defaultColor = 'rgba(217, 217, 217, 1)'
|
||||
): string => {
|
||||
return sortBy(legend.rules, 'sortBy').reduce((color: string, rule) => {
|
||||
return rules.reduce((color: string, rule) => {
|
||||
const operatorFn = OPERATOR_TO_FN[rule.operator];
|
||||
if (operatorFn(value, rule.value)) {
|
||||
return rule.color;
|
||||
|
@ -65,19 +97,19 @@ export const calculateStepColor = (
|
|||
};
|
||||
|
||||
export const calculateGradientColor = (
|
||||
legend: InfraWaffleMapGradientLegend,
|
||||
rules: InfraWaffleMapGradientRule[],
|
||||
value: number | string,
|
||||
bounds: InfraWaffleMapBounds,
|
||||
defaultColor = 'rgba(0, 179, 164, 1)'
|
||||
): string => {
|
||||
if (legend.rules.length === 0) {
|
||||
if (rules.length === 0) {
|
||||
return defaultColor;
|
||||
}
|
||||
if (legend.rules.length === 1) {
|
||||
return last(legend.rules).color;
|
||||
if (rules.length === 1) {
|
||||
return last(rules).color;
|
||||
}
|
||||
const { min, max } = bounds;
|
||||
const sortedRules = sortBy(legend.rules, 'value');
|
||||
const sortedRules = sortBy(rules, 'value');
|
||||
const normValue = normalizeValue(min, max, Number(value));
|
||||
const startRule = sortedRules.reduce((acc, rule) => {
|
||||
if (rule.value <= normValue) {
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { InfraWaffleMapBounds } from '../../../../lib/lib';
|
||||
export const convertBoundsToPercents = (bounds: InfraWaffleMapBounds) => ({
|
||||
min: bounds.min * 100,
|
||||
max: (bounds.max || 1) * 100,
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { InventoryColorPalette, InfraWaffleMapSteppedGradientLegend } from '../../../../lib/lib';
|
||||
import { getColorPalette } from './get_color_palette';
|
||||
|
||||
export const createLegend = (
|
||||
name: InventoryColorPalette,
|
||||
steps: number = 10,
|
||||
reverse: boolean = false
|
||||
): InfraWaffleMapSteppedGradientLegend => {
|
||||
const paletteColors = getColorPalette(name, steps, reverse);
|
||||
return {
|
||||
type: 'steppedGradient',
|
||||
rules: paletteColors.map((color, index) => ({
|
||||
color,
|
||||
value: (index + 1) / steps,
|
||||
})),
|
||||
};
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
euiPaletteCool,
|
||||
euiPaletteForStatus,
|
||||
euiPaletteForTemperature,
|
||||
euiPaletteNegative,
|
||||
euiPalettePositive,
|
||||
euiPaletteWarm,
|
||||
} from '@elastic/eui';
|
||||
import { InventoryColorPalette } from '../../../../lib/lib';
|
||||
|
||||
const createColorPalette = (name: InventoryColorPalette = 'cool', steps: number = 10) => {
|
||||
switch (name) {
|
||||
case 'temperature':
|
||||
return euiPaletteForTemperature(steps);
|
||||
case 'status':
|
||||
return euiPaletteForStatus(steps);
|
||||
case 'warm':
|
||||
return euiPaletteWarm(steps);
|
||||
case 'positive':
|
||||
return euiPalettePositive(steps);
|
||||
case 'negative':
|
||||
return euiPaletteNegative(steps);
|
||||
default:
|
||||
return euiPaletteCool(steps);
|
||||
}
|
||||
};
|
||||
|
||||
export const getColorPalette = (
|
||||
name: InventoryColorPalette = 'cool',
|
||||
steps: number = 10,
|
||||
reverse: boolean = false
|
||||
) => {
|
||||
return reverse ? createColorPalette(name, steps).reverse() : createColorPalette(name, steps);
|
||||
};
|
|
@ -7641,7 +7641,6 @@
|
|||
"xpack.infra.kibanaMetrics.nodeDoesNotExistErrorMessage": "{nodeId} が存在しません。",
|
||||
"xpack.infra.legendControls.applyButton": "適用",
|
||||
"xpack.infra.legendControls.buttonLabel": "凡例を校正",
|
||||
"xpack.infra.legendControls.errorMessage": "最低値は最高値よりも低く設定する必要があります",
|
||||
"xpack.infra.legendControls.maxLabel": "最高",
|
||||
"xpack.infra.legendControls.minLabel": "最低",
|
||||
"xpack.infra.legendControls.switchLabel": "自動計算範囲",
|
||||
|
|
|
@ -7647,7 +7647,6 @@
|
|||
"xpack.infra.kibanaMetrics.nodeDoesNotExistErrorMessage": "{nodeId} 不存在。",
|
||||
"xpack.infra.legendControls.applyButton": "应用",
|
||||
"xpack.infra.legendControls.buttonLabel": "配置图例",
|
||||
"xpack.infra.legendControls.errorMessage": "最小值应小于最大值",
|
||||
"xpack.infra.legendControls.maxLabel": "最大值",
|
||||
"xpack.infra.legendControls.minLabel": "最小值",
|
||||
"xpack.infra.legendControls.switchLabel": "自动计算范围",
|
||||
|
|
Loading…
Reference in a new issue