mirror of
https://github.com/grafana/grafana.git
synced 2024-12-21 22:53:59 -06:00
Explore Metrics: Introduce augurs sorting options in breakdown view (#91189)
* refactor breakdown scene * refactor BreakdownScene along with LayoutSwitcher * rename * don't pass default layout * better type handling * betterer * add @bsull/augurs * implement LabelBreakdownScene * integrate SortByScene in LabelBreakdownScene * move to new directory * introduce BreakdownSearchScene * integrate searchScene * cleaning * initialize @bsull/augurs * add interaction * use new breakdown scene * resolve merge conflicts * ugrade @bsull/augurs * update import * update css * update tooltip text * refine sorting * fix unit test * fix * implement outlier detector * support wasm * jest testing fix * localization fix * use unknown instead of any * update i18n * update betterer * fix locales * update test * fix tests maybe * prettier * chore: update jest config * chore: create mock for @bsull/augurs (#92156) chore: create mock for bsull/augurs @bsull/augurs assumes it will be running as an ESM, not a CommonJS module, so can't be loaded by Jest (specifically because it contains a reference to import.meta.url). This PR provides a mock implementation which gets tests passing again. Ideally we'd be able to load the actual @bsull/augurs module in tests so this is just a stopgap really, until a better solution appears. * fix unit tests * remove unused BreakdownScene.tsx * set outliers as undefined if an error occurs * Add labels * betterer * reset event implemented * fix controls positioning * update augurs * betterer * i18n * conflict fixes * update texts --------- Co-authored-by: Matias Chomicki <matyax@gmail.com> Co-authored-by: Ben Sully <ben.sully@grafana.com>
This commit is contained in:
parent
297ccfc52c
commit
bcdcb1f74b
@ -4362,6 +4362,9 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
|
||||
],
|
||||
"public/app/features/trails/services/sorting.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/transformers/FilterByValueTransformer/FilterByValueTransformerEditor.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||
],
|
||||
|
@ -20,6 +20,7 @@ const esModules = [
|
||||
'@msagl',
|
||||
'lodash-es',
|
||||
'vscode-languageserver-types',
|
||||
'@bsull/augurs',
|
||||
].join('|');
|
||||
|
||||
module.exports = {
|
||||
@ -54,6 +55,7 @@ module.exports = {
|
||||
'^@grafana/schema/dist/esm/(.*)$': '<rootDir>/packages/grafana-schema/src/$1',
|
||||
// prevent systemjs amd extra from breaking tests.
|
||||
'systemjs/dist/extras/amd': '<rootDir>/public/test/mocks/systemjsAMDExtra.ts',
|
||||
'@bsull/augurs': '<rootDir>/public/test/mocks/augurs.ts',
|
||||
},
|
||||
// Log the test results with dynamic Loki tags. Drone CI only
|
||||
reporters: ['default', ['<rootDir>/public/test/log-reporter.js', { enable: process.env.DRONE === 'true' }]],
|
||||
|
@ -245,6 +245,7 @@
|
||||
"yargs": "^17.5.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bsull/augurs": "0.4.2",
|
||||
"@emotion/css": "11.13.4",
|
||||
"@emotion/react": "11.13.3",
|
||||
"@fingerprintjs/fingerprintjs": "^3.4.2",
|
||||
|
@ -18,6 +18,7 @@ import { Trans } from 'app/core/internationalization';
|
||||
|
||||
import { getLabelValueFromDataFrame } from '../services/levels';
|
||||
import { fuzzySearch } from '../services/search';
|
||||
import { sortSeries } from '../services/sorting';
|
||||
|
||||
import { BreakdownSearchReset } from './BreakdownSearchScene';
|
||||
import { findSceneObjectsByType } from './utils';
|
||||
@ -33,12 +34,18 @@ type FrameIterateCallback = (frames: DataFrame[], seriesIndex: number) => void;
|
||||
|
||||
export class ByFrameRepeater extends SceneObjectBase<ByFrameRepeaterState> {
|
||||
private unfilteredChildren: SceneFlexItem[] = [];
|
||||
private series: DataFrame[] = [];
|
||||
private sortBy: string;
|
||||
private sortedSeries: DataFrame[] = [];
|
||||
private getFilter: () => string;
|
||||
|
||||
public constructor({ getFilter, ...state }: ByFrameRepeaterState & { getFilter: () => string }) {
|
||||
public constructor({
|
||||
sortBy,
|
||||
getFilter,
|
||||
...state
|
||||
}: ByFrameRepeaterState & { sortBy: string; getFilter: () => string }) {
|
||||
super(state);
|
||||
|
||||
this.sortBy = sortBy;
|
||||
this.getFilter = getFilter;
|
||||
|
||||
this.addActivationHandler(() => {
|
||||
@ -69,15 +76,24 @@ export class ByFrameRepeater extends SceneObjectBase<ByFrameRepeaterState> {
|
||||
});
|
||||
}
|
||||
|
||||
public sort = (sortBy: string) => {
|
||||
const data = sceneGraph.getData(this);
|
||||
this.sortBy = sortBy;
|
||||
if (data.state.data) {
|
||||
this.performRepeat(data.state.data);
|
||||
}
|
||||
};
|
||||
|
||||
private performRepeat(data: PanelData) {
|
||||
const newChildren: SceneFlexItem[] = [];
|
||||
this.series = data.series;
|
||||
const sortedSeries = sortSeries(data.series, this.sortBy);
|
||||
|
||||
for (let seriesIndex = 0; seriesIndex < this.series.length; seriesIndex++) {
|
||||
const layoutChild = this.state.getLayoutChild(data, this.series[seriesIndex], seriesIndex);
|
||||
for (let seriesIndex = 0; seriesIndex < sortedSeries.length; seriesIndex++) {
|
||||
const layoutChild = this.state.getLayoutChild(data, sortedSeries[seriesIndex], seriesIndex);
|
||||
newChildren.push(layoutChild);
|
||||
}
|
||||
|
||||
this.sortedSeries = sortedSeries;
|
||||
this.unfilteredChildren = newChildren;
|
||||
|
||||
if (this.getFilter()) {
|
||||
@ -114,8 +130,8 @@ export class ByFrameRepeater extends SceneObjectBase<ByFrameRepeaterState> {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
for (let seriesIndex = 0; seriesIndex < this.series.length; seriesIndex++) {
|
||||
callback(this.series, seriesIndex);
|
||||
for (let seriesIndex = 0; seriesIndex < this.sortedSeries.length; seriesIndex++) {
|
||||
callback(this.sortedSeries, seriesIndex);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import init from '@bsull/augurs';
|
||||
import { css } from '@emotion/css';
|
||||
import { isNumber, max, min, throttle } from 'lodash';
|
||||
import { useEffect } from 'react';
|
||||
@ -35,6 +36,7 @@ import { MetricScene } from '../MetricScene';
|
||||
import { StatusWrapper } from '../StatusWrapper';
|
||||
import { reportExploreMetrics } from '../interactions';
|
||||
import { updateOtelJoinWithGroupLeft } from '../otel/util';
|
||||
import { getSortByPreference } from '../services/store';
|
||||
import { ALL_VARIABLE_VALUE } from '../services/variables';
|
||||
import {
|
||||
MDP_METRIC_PREVIEW,
|
||||
@ -50,6 +52,7 @@ import { AddToFiltersGraphAction } from './AddToFiltersGraphAction';
|
||||
import { BreakdownSearchReset, BreakdownSearchScene } from './BreakdownSearchScene';
|
||||
import { ByFrameRepeater } from './ByFrameRepeater';
|
||||
import { LayoutSwitcher } from './LayoutSwitcher';
|
||||
import { SortByScene, SortCriteriaChanged } from './SortByScene';
|
||||
import { BreakdownLayoutChangeCallback, BreakdownLayoutType } from './types';
|
||||
import { getLabelOptions } from './utils';
|
||||
import { BreakdownAxisChangeEvent, yAxisSyncBehavior } from './yAxisSyncBehavior';
|
||||
@ -59,6 +62,7 @@ const MAX_PANELS_IN_ALL_LABELS_BREAKDOWN = 60;
|
||||
export interface LabelBreakdownSceneState extends SceneObjectState {
|
||||
body?: LayoutSwitcher;
|
||||
search: BreakdownSearchScene;
|
||||
sortBy: SortByScene;
|
||||
labels: Array<SelectableValue<string>>;
|
||||
value?: string;
|
||||
loading?: boolean;
|
||||
@ -76,6 +80,7 @@ export class LabelBreakdownScene extends SceneObjectBase<LabelBreakdownSceneStat
|
||||
super({
|
||||
...state,
|
||||
labels: state.labels ?? [],
|
||||
sortBy: new SortByScene({ target: 'labels' }),
|
||||
search: new BreakdownSearchScene('labels'),
|
||||
});
|
||||
|
||||
@ -85,6 +90,9 @@ export class LabelBreakdownScene extends SceneObjectBase<LabelBreakdownSceneStat
|
||||
private _query?: AutoQueryDef;
|
||||
|
||||
private _onActivate() {
|
||||
// eslint-disable-next-line no-console
|
||||
init().then(() => console.debug('Grafana ML initialized'));
|
||||
|
||||
const variable = this.getVariable();
|
||||
|
||||
variable.subscribeToState((newState, oldState) => {
|
||||
@ -102,6 +110,7 @@ export class LabelBreakdownScene extends SceneObjectBase<LabelBreakdownSceneStat
|
||||
this.state.search.clearValueFilter();
|
||||
})
|
||||
);
|
||||
this._subs.add(this.subscribeToEvent(SortCriteriaChanged, this.handleSortByChange));
|
||||
|
||||
const metricScene = sceneGraph.getAncestor(this, MetricScene);
|
||||
const metric = metricScene.state.metric;
|
||||
@ -204,6 +213,20 @@ export class LabelBreakdownScene extends SceneObjectBase<LabelBreakdownSceneStat
|
||||
return variable;
|
||||
}
|
||||
|
||||
private handleSortByChange = (event: SortCriteriaChanged) => {
|
||||
if (event.target !== 'labels') {
|
||||
return;
|
||||
}
|
||||
if (this.state.body instanceof LayoutSwitcher) {
|
||||
this.state.body.state.breakdownLayouts.forEach((layout) => {
|
||||
if (layout instanceof ByFrameRepeater) {
|
||||
layout.sort(event.sortBy);
|
||||
}
|
||||
});
|
||||
}
|
||||
reportExploreMetrics('sorting_changed', { sortBy: event.sortBy });
|
||||
};
|
||||
|
||||
private onReferencedVariableValueChanged() {
|
||||
const variable = this.getVariable();
|
||||
variable.changeValueTo(ALL_VARIABLE_VALUE);
|
||||
@ -291,7 +314,7 @@ export class LabelBreakdownScene extends SceneObjectBase<LabelBreakdownSceneStat
|
||||
}
|
||||
|
||||
public static Component = ({ model }: SceneComponentProps<LabelBreakdownScene>) => {
|
||||
const { labels, body, search, loading, value, blockingMessage } = model.useState();
|
||||
const { labels, body, search, sortBy, loading, value, blockingMessage } = model.useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const trail = getTrailFor(model);
|
||||
@ -322,9 +345,12 @@ export class LabelBreakdownScene extends SceneObjectBase<LabelBreakdownSceneStat
|
||||
)}
|
||||
|
||||
{value !== ALL_VARIABLE_VALUE && (
|
||||
<Field label="Search" className={styles.searchField}>
|
||||
<search.Component model={search} />
|
||||
</Field>
|
||||
<>
|
||||
<Field label="Search" className={styles.searchField}>
|
||||
<search.Component model={search} />
|
||||
</Field>
|
||||
<sortBy.Component model={sortBy} />
|
||||
</>
|
||||
)}
|
||||
{body instanceof LayoutSwitcher && (
|
||||
<Field label="View">
|
||||
@ -470,6 +496,7 @@ function buildNormalLayout(
|
||||
return item;
|
||||
}
|
||||
|
||||
const { sortBy } = getSortByPreference('labels', 'outliers');
|
||||
const getFilter = () => searchScene.state.filter ?? '';
|
||||
|
||||
return new LayoutSwitcher({
|
||||
@ -511,6 +538,7 @@ function buildNormalLayout(
|
||||
],
|
||||
}),
|
||||
getLayoutChild,
|
||||
sortBy,
|
||||
getFilter,
|
||||
}),
|
||||
new ByFrameRepeater({
|
||||
@ -520,6 +548,7 @@ function buildNormalLayout(
|
||||
children: [],
|
||||
}),
|
||||
getLayoutChild,
|
||||
sortBy,
|
||||
getFilter,
|
||||
}),
|
||||
],
|
||||
|
109
public/app/features/trails/Breakdown/SortByScene.tsx
Normal file
109
public/app/features/trails/Breakdown/SortByScene.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { BusEventBase, GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
|
||||
import { IconButton, Select } from '@grafana/ui';
|
||||
import { Field, useStyles2 } from '@grafana/ui/';
|
||||
|
||||
import { Trans } from '../../../core/internationalization';
|
||||
import { getSortByPreference, setSortByPreference } from '../services/store';
|
||||
|
||||
export interface SortBySceneState extends SceneObjectState {
|
||||
target: 'fields' | 'labels';
|
||||
sortBy: string;
|
||||
}
|
||||
|
||||
export class SortCriteriaChanged extends BusEventBase {
|
||||
constructor(
|
||||
public target: 'fields' | 'labels',
|
||||
public sortBy: string
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public static type = 'sort-criteria-changed';
|
||||
}
|
||||
|
||||
export class SortByScene extends SceneObjectBase<SortBySceneState> {
|
||||
public sortingOptions = [
|
||||
{
|
||||
label: '',
|
||||
options: [
|
||||
{
|
||||
value: 'outliers',
|
||||
label: 'Outlying Values',
|
||||
description: 'Prioritizes values that show distinct behavior from others within the same label',
|
||||
},
|
||||
{
|
||||
value: 'alphabetical',
|
||||
label: 'Name [A-Z]',
|
||||
description: 'Alphabetical order',
|
||||
},
|
||||
{
|
||||
value: 'alphabetical-reversed',
|
||||
label: 'Name [Z-A]',
|
||||
description: 'Reversed alphabetical order',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
constructor(state: Pick<SortBySceneState, 'target'>) {
|
||||
const { sortBy } = getSortByPreference(state.target, 'outliers');
|
||||
super({
|
||||
target: state.target,
|
||||
sortBy,
|
||||
});
|
||||
}
|
||||
|
||||
public onCriteriaChange = (criteria: SelectableValue<string>) => {
|
||||
if (!criteria.value) {
|
||||
return;
|
||||
}
|
||||
this.setState({ sortBy: criteria.value });
|
||||
setSortByPreference(this.state.target, criteria.value);
|
||||
this.publishEvent(new SortCriteriaChanged(this.state.target, criteria.value), true);
|
||||
};
|
||||
|
||||
public static Component = ({ model }: SceneComponentProps<SortByScene>) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { sortBy } = model.useState();
|
||||
const group = model.sortingOptions.find((group) => group.options.find((option) => option.value === sortBy));
|
||||
const value = group?.options.find((option) => option.value === sortBy);
|
||||
return (
|
||||
<Field
|
||||
htmlFor="sort-by-criteria"
|
||||
label={
|
||||
<div className={styles.sortByTooltip}>
|
||||
<Trans i18nKey="explore-metrics.breakdown.sortBy">Sort by</Trans>
|
||||
<IconButton
|
||||
name={'info-circle'}
|
||||
size="sm"
|
||||
variant={'secondary'}
|
||||
tooltip="Sorts values using standard or smart time series calculations."
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Select
|
||||
value={value}
|
||||
width={20}
|
||||
isSearchable={true}
|
||||
options={model.sortingOptions}
|
||||
placeholder={'Choose criteria'}
|
||||
onChange={model.onCriteriaChange}
|
||||
inputId="sort-by-criteria"
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
sortByTooltip: css({
|
||||
display: 'flex',
|
||||
gap: theme.spacing(1),
|
||||
}),
|
||||
};
|
||||
}
|
@ -111,6 +111,10 @@ type Interactions = {
|
||||
| 'close'
|
||||
)
|
||||
};
|
||||
sorting_changed: {
|
||||
// type of sorting
|
||||
sortBy: string
|
||||
};
|
||||
wasm_not_supported: {},
|
||||
};
|
||||
|
||||
|
74
public/app/features/trails/services/sorting.test.ts
Normal file
74
public/app/features/trails/services/sorting.test.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { toDataFrame, FieldType, ReducerID } from '@grafana/data';
|
||||
|
||||
import { sortSeries } from './sorting';
|
||||
|
||||
const frameA = toDataFrame({
|
||||
fields: [
|
||||
{ name: 'Time', type: FieldType.time, values: [0] },
|
||||
{
|
||||
name: 'Value',
|
||||
type: FieldType.number,
|
||||
values: [0, 1, 0],
|
||||
labels: {
|
||||
test: 'C',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const frameB = toDataFrame({
|
||||
fields: [
|
||||
{ name: 'Time', type: FieldType.time, values: [0] },
|
||||
{
|
||||
name: 'Value',
|
||||
type: FieldType.number,
|
||||
values: [1, 1, 1],
|
||||
labels: {
|
||||
test: 'A',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const frameC = toDataFrame({
|
||||
fields: [
|
||||
{ name: 'Time', type: FieldType.time, values: [0] },
|
||||
{
|
||||
name: 'Value',
|
||||
type: FieldType.number,
|
||||
values: [100, 9999, 100],
|
||||
labels: {
|
||||
test: 'B',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
describe('sortSeries', () => {
|
||||
test('Sorts series by standard deviation, descending', () => {
|
||||
const series = [frameA, frameB, frameC];
|
||||
const sortedSeries = [frameC, frameA, frameB];
|
||||
|
||||
const result = sortSeries(series, ReducerID.stdDev, 'desc');
|
||||
expect(result).toEqual(sortedSeries);
|
||||
});
|
||||
test('Sorts series by standard deviation, ascending', () => {
|
||||
const series = [frameA, frameB, frameC];
|
||||
const sortedSeries = [frameB, frameA, frameC];
|
||||
|
||||
const result = sortSeries(series, ReducerID.stdDev, 'asc');
|
||||
expect(result).toEqual(sortedSeries);
|
||||
});
|
||||
test('Sorts series alphabetically, ascending', () => {
|
||||
const series = [frameA, frameB, frameC];
|
||||
const sortedSeries = [frameB, frameC, frameA];
|
||||
|
||||
const result = sortSeries(series, 'alphabetical', 'asc');
|
||||
expect(result).toEqual(sortedSeries);
|
||||
});
|
||||
test('Sorts series alphabetically, descending', () => {
|
||||
const series = [frameA, frameB, frameC];
|
||||
const sortedSeries = [frameB, frameC, frameA];
|
||||
|
||||
const result = sortSeries(series, 'alphabetical', 'desc');
|
||||
expect(result).toEqual(sortedSeries);
|
||||
});
|
||||
});
|
138
public/app/features/trails/services/sorting.ts
Normal file
138
public/app/features/trails/services/sorting.ts
Normal file
@ -0,0 +1,138 @@
|
||||
import { OutlierDetector, OutlierOutput } from '@bsull/augurs';
|
||||
import { memoize } from 'lodash';
|
||||
|
||||
import { DataFrame, doStandardCalcs, fieldReducers, FieldType, outerJoinDataFrames, ReducerID } from '@grafana/data';
|
||||
|
||||
import { reportExploreMetrics } from '../interactions';
|
||||
|
||||
import { getLabelValueFromDataFrame } from './levels';
|
||||
|
||||
export const sortSeries = memoize(
|
||||
(series: DataFrame[], sortBy: string, direction = 'asc') => {
|
||||
if (sortBy === 'alphabetical') {
|
||||
return sortSeriesByName(series, 'asc');
|
||||
}
|
||||
|
||||
if (sortBy === 'alphabetical-reversed') {
|
||||
return sortSeriesByName(series, 'desc');
|
||||
}
|
||||
|
||||
if (sortBy === 'outliers') {
|
||||
initOutlierDetector(series);
|
||||
}
|
||||
|
||||
const reducer = (dataFrame: DataFrame) => {
|
||||
try {
|
||||
if (sortBy === 'outliers') {
|
||||
return calculateOutlierValue(series, dataFrame);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// ML sorting panicked, fallback to stdDev
|
||||
sortBy = ReducerID.stdDev;
|
||||
}
|
||||
const fieldReducer = fieldReducers.get(sortBy);
|
||||
const value =
|
||||
fieldReducer.reduce?.(dataFrame.fields[1], true, true) ?? doStandardCalcs(dataFrame.fields[1], true, true);
|
||||
return value[sortBy] ?? 0;
|
||||
};
|
||||
|
||||
const seriesCalcs = series.map((dataFrame) => ({
|
||||
value: reducer(dataFrame),
|
||||
dataFrame: dataFrame,
|
||||
}));
|
||||
|
||||
seriesCalcs.sort((a, b) => {
|
||||
if (a.value !== undefined && b.value !== undefined) {
|
||||
return b.value - a.value;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (direction === 'asc') {
|
||||
seriesCalcs.reverse();
|
||||
}
|
||||
|
||||
return seriesCalcs.map(({ dataFrame }) => dataFrame);
|
||||
},
|
||||
(series: DataFrame[], sortBy: string, direction = 'asc') => {
|
||||
const firstTimestamp = series.length > 0 ? series[0].fields[0].values[0] : 0;
|
||||
const lastTimestamp =
|
||||
series.length > 0
|
||||
? series[series.length - 1].fields[0].values[series[series.length - 1].fields[0].values.length - 1]
|
||||
: 0;
|
||||
const firstValue = series.length > 0 ? getLabelValueFromDataFrame(series[0]) : '';
|
||||
const lastValue = series.length > 0 ? getLabelValueFromDataFrame(series[series.length - 1]) : '';
|
||||
const key = `${firstValue}_${lastValue}_${firstTimestamp}_${lastTimestamp}_${series.length}_${sortBy}_${direction}`;
|
||||
return key;
|
||||
}
|
||||
);
|
||||
|
||||
const initOutlierDetector = (series: DataFrame[]) => {
|
||||
if (!wasmSupported()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Combine all frames into one by joining on time.
|
||||
const joined = outerJoinDataFrames({ frames: series });
|
||||
if (!joined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get number fields: these are our series.
|
||||
const joinedSeries = joined.fields.filter((f) => f.type === FieldType.number);
|
||||
const nTimestamps = joinedSeries[0].values.length;
|
||||
const points = new Float64Array(joinedSeries.flatMap((series) => series.values as number[]));
|
||||
|
||||
try {
|
||||
const detector = OutlierDetector.dbscan({ sensitivity: 0.4 }).preprocess(points, nTimestamps);
|
||||
outliers = detector.detect();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
outliers = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
let outliers: OutlierOutput | undefined = undefined;
|
||||
|
||||
export const calculateOutlierValue = (series: DataFrame[], data: DataFrame): number => {
|
||||
if (!wasmSupported()) {
|
||||
throw new Error('WASM not supported, fall back to stdDev');
|
||||
}
|
||||
if (!outliers) {
|
||||
throw new Error('Initialize outlier detector first');
|
||||
}
|
||||
|
||||
const index = series.indexOf(data);
|
||||
if (outliers.seriesResults[index].isOutlier) {
|
||||
return outliers.seriesResults[index].outlierIntervals.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const sortSeriesByName = (series: DataFrame[], direction: string) => {
|
||||
const sortedSeries = [...series];
|
||||
sortedSeries.sort((a, b) => {
|
||||
const valueA = getLabelValueFromDataFrame(a);
|
||||
const valueB = getLabelValueFromDataFrame(b);
|
||||
if (!valueA || !valueB) {
|
||||
return 0;
|
||||
}
|
||||
return valueA?.localeCompare(valueB) ?? 0;
|
||||
});
|
||||
if (direction === 'desc') {
|
||||
sortedSeries.reverse();
|
||||
}
|
||||
return sortedSeries;
|
||||
};
|
||||
|
||||
export const wasmSupported = () => {
|
||||
const support = typeof WebAssembly === 'object';
|
||||
|
||||
if (!support) {
|
||||
reportExploreMetrics('wasm_not_supported', {});
|
||||
}
|
||||
|
||||
return support;
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import { TRAIL_BREAKDOWN_VIEW_KEY } from '../shared';
|
||||
import { TRAIL_BREAKDOWN_VIEW_KEY, TRAIL_BREAKDOWN_SORT_KEY } from '../shared';
|
||||
|
||||
export function getVewByPreference() {
|
||||
return localStorage.getItem(TRAIL_BREAKDOWN_VIEW_KEY) ?? 'grid';
|
||||
@ -7,3 +7,19 @@ export function getVewByPreference() {
|
||||
export function setVewByPreference(value?: string) {
|
||||
return localStorage.setItem(TRAIL_BREAKDOWN_VIEW_KEY, value ?? 'grid');
|
||||
}
|
||||
|
||||
export function getSortByPreference(target: string, defaultSortBy: string) {
|
||||
const preference = localStorage.getItem(`${TRAIL_BREAKDOWN_SORT_KEY}.${target}.by`) ?? '';
|
||||
const parts = preference.split('.');
|
||||
if (!parts[0] || !parts[1]) {
|
||||
return { sortBy: defaultSortBy };
|
||||
}
|
||||
return { sortBy: parts[0], direction: parts[1] };
|
||||
}
|
||||
|
||||
export function setSortByPreference(target: string, sortBy: string) {
|
||||
// Prevent storing empty values
|
||||
if (sortBy) {
|
||||
localStorage.setItem(`${TRAIL_BREAKDOWN_SORT_KEY}.${target}.by`, `${sortBy}`);
|
||||
}
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ export const trailDS = { uid: VAR_DATASOURCE_EXPR };
|
||||
export const RECENT_TRAILS_KEY = 'grafana.trails.recent';
|
||||
export const TRAIL_BOOKMARKS_KEY = 'grafana.trails.bookmarks';
|
||||
export const TRAIL_BREAKDOWN_VIEW_KEY = 'grafana.trails.breakdown.view';
|
||||
export const TRAIL_BREAKDOWN_SORT_KEY = 'grafana.trails.breakdown.sort';
|
||||
|
||||
export const MDP_METRIC_PREVIEW = 250;
|
||||
export const MDP_METRIC_OVERVIEW = 500;
|
||||
|
@ -1049,7 +1049,8 @@
|
||||
"breakdown": {
|
||||
"clearFilter": "Clear filter",
|
||||
"labelSelect": "Select",
|
||||
"noMatchingValue": "No values found matching; {{filter}}"
|
||||
"noMatchingValue": "No values found matching; {{filter}}",
|
||||
"sortBy": "Sort by"
|
||||
},
|
||||
"viewBy": "View by"
|
||||
},
|
||||
|
@ -1049,7 +1049,8 @@
|
||||
"breakdown": {
|
||||
"clearFilter": "Cľęäř ƒįľŧęř",
|
||||
"labelSelect": "Ŝęľęčŧ",
|
||||
"noMatchingValue": "Ńő väľūęş ƒőūʼnđ mäŧčĥįʼnģ; {{filter}}"
|
||||
"noMatchingValue": "Ńő väľūęş ƒőūʼnđ mäŧčĥįʼnģ; {{filter}}",
|
||||
"sortBy": "Ŝőřŧ þy"
|
||||
},
|
||||
"viewBy": "Vįęŵ þy"
|
||||
},
|
||||
|
32
public/test/mocks/augurs.ts
Normal file
32
public/test/mocks/augurs.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import type {
|
||||
LoadedOutlierDetector as AugursLoadedOutlierDetector,
|
||||
OutlierDetector as AugursOutlierDetector,
|
||||
OutlierDetectorOptions,
|
||||
OutlierOutput,
|
||||
} from '@bsull/augurs';
|
||||
|
||||
export default function init() {}
|
||||
|
||||
const dummyOutliers: OutlierOutput = {
|
||||
outlyingSeries: [],
|
||||
clusterBand: { min: [], max: [] },
|
||||
seriesResults: [],
|
||||
};
|
||||
|
||||
export class OutlierDetector implements AugursOutlierDetector {
|
||||
free(): void {}
|
||||
detect(): OutlierOutput {
|
||||
return dummyOutliers;
|
||||
}
|
||||
preprocess(y: Float64Array, nTimestamps: number): AugursLoadedOutlierDetector {
|
||||
return new LoadedOutlierDetector();
|
||||
}
|
||||
}
|
||||
|
||||
export class LoadedOutlierDetector implements AugursLoadedOutlierDetector {
|
||||
detect(): OutlierOutput {
|
||||
return dummyOutliers;
|
||||
}
|
||||
free(): void {}
|
||||
updateDetector(options: OutlierDetectorOptions): void {}
|
||||
}
|
@ -9,6 +9,10 @@ module.exports = {
|
||||
app: './public/app/index.ts',
|
||||
swagger: './public/swagger/index.tsx',
|
||||
},
|
||||
experiments: {
|
||||
// Required to load WASM modules.
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
output: {
|
||||
clean: true,
|
||||
path: path.resolve(__dirname, '../../public/build'),
|
||||
|
@ -1703,6 +1703,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@bsull/augurs@npm:0.4.2":
|
||||
version: 0.4.2
|
||||
resolution: "@bsull/augurs@npm:0.4.2"
|
||||
checksum: 10/b4806fed1daf76380577b4ca86c2bf94c00ce1033df0b8365229a55ff16bd2f864d3f9ca3f92faf284e62013b68e953be65ee78968931ba342be0e1c054c2e63
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@bundled-es-modules/cookie@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "@bundled-es-modules/cookie@npm:2.0.0"
|
||||
@ -19049,6 +19056,7 @@ __metadata:
|
||||
"@betterer/betterer": "npm:5.4.0"
|
||||
"@betterer/cli": "npm:5.4.0"
|
||||
"@betterer/eslint": "npm:5.4.0"
|
||||
"@bsull/augurs": "npm:0.4.2"
|
||||
"@cypress/webpack-preprocessor": "npm:6.0.2"
|
||||
"@emotion/css": "npm:11.13.4"
|
||||
"@emotion/eslint-plugin": "npm:11.12.0"
|
||||
|
Loading…
Reference in New Issue
Block a user