Correlations: Enable create and edit using app platform (#117505)

* WIP

* Make transformations properties optional, create correct object for partial spec

* update the generated client type, fix logic for submission of transformations

* Update readme with what is needed for updating the client

* Move logic to utils, add test

* Add tests to increase coverage

* continue to build out test coverage

* Add tests

* Add create correlation

* WIP improve function coverage

* fix test

* wip add notification on edit

* Moving directories

* Move back as we are no longer rendering with a wrapper

* Run the generator
This commit is contained in:
Kristina
2026-02-26 07:39:26 -06:00
committed by GitHub
parent e955359887
commit 702f6d27ee
17 changed files with 425 additions and 94 deletions
+3 -3
View File
@@ -43,9 +43,9 @@ TargetSpec: {
TransformationSpec: {
type: "regex" | "logfmt"
expression: string
field: string
mapValue: string
expression?: string
field?: string
mapValue?: string
}
CorrelationType: "query" | "external"
@@ -62,9 +62,9 @@ func (CorrelationTargetSpec) OpenAPIModelName() string {
// +k8s:openapi-gen=true
type CorrelationTransformationSpec struct {
Type CorrelationTransformationSpecType `json:"type"`
Expression string `json:"expression"`
Field string `json:"field"`
MapValue string `json:"mapValue"`
Expression *string `json:"expression,omitempty"`
Field *string `json:"field,omitempty"`
MapValue *string `json:"mapValue,omitempty"`
}
// NewCorrelationTransformationSpec creates a new CorrelationTransformationSpec object.
+1 -1
View File
@@ -20,7 +20,7 @@ import (
)
var (
rawSchemaCorrelationv0alpha1 = []byte(`{"ConfigSpec":{"additionalProperties":false,"description":"there was a deprecated field here called type, we will need to move that for conversion and provisioning","properties":{"field":{"type":"string"},"target":{"$ref":"#/components/schemas/TargetSpec"},"transformations":{"items":{"$ref":"#/components/schemas/TransformationSpec"},"type":"array"}},"required":["field","target"],"type":"object"},"Correlation":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"CorrelationType":{"enum":["query","external"],"type":"string"},"DataSourceRef":{"additionalProperties":false,"properties":{"group":{"description":"same as pluginId","type":"string"},"name":{"description":"same as grafana uid","type":"string"}},"required":["group","name"],"type":"object"},"TargetSpec":{"additionalProperties":true,"type":"object"},"TransformationSpec":{"additionalProperties":false,"properties":{"expression":{"type":"string"},"field":{"type":"string"},"mapValue":{"type":"string"},"type":{"enum":["regex","logfmt"],"type":"string"}},"required":["type","expression","field","mapValue"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"config":{"$ref":"#/components/schemas/ConfigSpec"},"description":{"type":"string"},"label":{"type":"string"},"source":{"$ref":"#/components/schemas/DataSourceRef"},"target":{"$ref":"#/components/schemas/DataSourceRef"},"type":{"$ref":"#/components/schemas/CorrelationType"}},"required":["type","source","label","config"],"type":"object"}}`)
rawSchemaCorrelationv0alpha1 = []byte(`{"ConfigSpec":{"additionalProperties":false,"description":"there was a deprecated field here called type, we will need to move that for conversion and provisioning","properties":{"field":{"type":"string"},"target":{"$ref":"#/components/schemas/TargetSpec"},"transformations":{"items":{"$ref":"#/components/schemas/TransformationSpec"},"type":"array"}},"required":["field","target"],"type":"object"},"Correlation":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"CorrelationType":{"enum":["query","external"],"type":"string"},"DataSourceRef":{"additionalProperties":false,"properties":{"group":{"description":"same as pluginId","type":"string"},"name":{"description":"same as grafana uid","type":"string"}},"required":["group","name"],"type":"object"},"TargetSpec":{"additionalProperties":true,"type":"object"},"TransformationSpec":{"additionalProperties":false,"properties":{"expression":{"type":"string"},"field":{"type":"string"},"mapValue":{"type":"string"},"type":{"enum":["regex","logfmt"],"type":"string"}},"required":["type"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"config":{"$ref":"#/components/schemas/ConfigSpec"},"description":{"type":"string"},"label":{"type":"string"},"source":{"$ref":"#/components/schemas/DataSourceRef"},"target":{"$ref":"#/components/schemas/DataSourceRef"},"type":{"$ref":"#/components/schemas/CorrelationType"}},"required":["type","source","label","config"],"type":"object"}}`)
versionSchemaCorrelationv0alpha1 app.VersionSchema
_ = json.Unmarshal(rawSchemaCorrelationv0alpha1, &versionSchemaCorrelationv0alpha1)
)
@@ -37,16 +37,13 @@ export const defaultTargetSpec = (): TargetSpec => ({});
export interface TransformationSpec {
type: "regex" | "logfmt";
expression: string;
field: string;
mapValue: string;
expression?: string;
field?: string;
mapValue?: string;
}
export const defaultTransformationSpec = (): TransformationSpec => ({
type: "regex",
expression: "",
field: "",
mapValue: "",
});
export interface Spec {
+1 -1
View File
@@ -29,4 +29,4 @@ Run `yarn generate:api-client` and follow the prompts. See [API Client Generator
## Updating generated clients
To update the existing clients, for example, when the OpenAPI spec has changed, run `yarn generate-apis`. This will regenerate all the clients based on the current OpenAPI snapshots.
To update the existing clients, for example, when the OpenAPI spec has changed, run `go test ./pkg/tests/apis -run TestIntegrationOpenAPIs`, followed by `yarn generate-apis`. This will regenerate all the clients based on the current OpenAPI snapshots.
@@ -411,9 +411,9 @@ export type CorrelationTargetSpec = {
[key: string]: any;
};
export type CorrelationTransformationSpec = {
expression: string;
field: string;
mapValue: string;
expression?: string;
field?: string;
mapValue?: string;
type: 'regex' | 'logfmt';
};
export type CorrelationConfigSpec = {
@@ -2,25 +2,4 @@ export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI';
import { generatedAPI as rawAPI } from './endpoints.gen';
export * from './endpoints.gen';
export const generatedAPI = rawAPI.enhanceEndpoints({
endpoints: {
createCorrelation: (endpointDefinition) => {
const originalQuery = endpointDefinition.query;
if (!originalQuery) {
return;
}
endpointDefinition.query = (requestOptions) => {
// Ensure metadata exists
if (!requestOptions.correlation.metadata) {
requestOptions.correlation.metadata = {};
}
const metadata = requestOptions.correlation.metadata;
if (!metadata.name && !metadata.generateName) {
// GenerateName lets the apiserver create a new uid for the name
metadata.generateName = 'c';
}
return originalQuery(requestOptions);
};
},
},
});
export const generatedAPI = rawAPI.enhanceEndpoints({});
@@ -953,10 +953,7 @@
"com.github.grafana.grafana.apps.correlations.pkg.apis.correlation.v0alpha1.CorrelationTransformationSpec": {
"type": "object",
"required": [
"type",
"expression",
"field",
"mapValue"
"type"
],
"properties": {
"expression": {
@@ -0,0 +1,43 @@
import { generatedAPI } from '@grafana/api-clients/rtkq/correlations/v0alpha1';
import { t } from '@grafana/i18n';
import { createErrorNotification, createSuccessNotification } from 'app/core/copy/appNotification';
import { notifyApp } from 'app/core/reducers/appNotification';
export const correlationsAPIv0alpha1 = generatedAPI.enhanceEndpoints({
endpoints: {
createCorrelation: (endpointDefinition) => {
const originalQuery = endpointDefinition.query;
if (!originalQuery) {
return;
}
endpointDefinition.query = (requestOptions) => {
// Ensure metadata exists
if (!requestOptions.correlation.metadata) {
requestOptions.correlation.metadata = {};
}
const metadata = requestOptions.correlation.metadata;
if (!metadata.name && !metadata.generateName) {
// GenerateName lets the apiserver create a new uid for the name
metadata.generateName = 'c';
}
return originalQuery(requestOptions);
};
},
updateCorrelation: {
onQueryStarted: async ({}, { queryFulfilled, dispatch }) => {
try {
await queryFulfilled;
dispatch(notifyApp(createSuccessNotification(t('correlation.edit-success', 'Correlation updated'))));
} catch (e) {
if (e instanceof Error) {
dispatch(notifyApp(createErrorNotification(t('correlation.edit-error', 'Error updating correlation'), e)));
}
}
},
},
},
});
// eslint-disable-next-line no-barrel-files/no-barrel-files
export * from '@grafana/api-clients/rtkq/correlations/v0alpha1';
@@ -25,8 +25,8 @@ import { useNavModel } from 'app/core/hooks/useNavModel';
import { contextSrv } from 'app/core/services/context_srv';
import { AccessControlAction } from 'app/types/accessControl';
import { AddCorrelationForm } from './Forms/AddCorrelationForm';
import { EditCorrelationForm } from './Forms/EditCorrelationForm';
import { AddCorrelationFormWrapper } from './Forms/AddCorrelationForm';
import { EditCorrelationFormWrapper } from './Forms/EditCorrelationForm';
import { EmptyCorrelationsCTA } from './components/EmptyCorrelationsCTA';
import type { Correlation, GetCorrelationsParams, RemoveCorrelationParams } from './types';
@@ -207,7 +207,7 @@ export default function CorrelationsPage(props: CorrelationsPageProps) {
</Alert>
)
}
{isAdding && <AddCorrelationForm onClose={() => setIsAdding(false)} onCreated={handleAdded} />}
{isAdding && <AddCorrelationFormWrapper onClose={() => setIsAdding(false)} onCreated={handleAdded} />}
{correlations && corrData.length >= 1 && (
<>
@@ -260,7 +260,7 @@ function ExpendedRow({ correlation: { source, ...correlation }, readOnly, onUpda
? { ...correlation, type: 'query', sourceUID: source.uid, targetUID: correlation.target.uid }
: { ...correlation, type: 'external', sourceUID: source.uid };
return <EditCorrelationForm correlation={corr} onUpdated={onUpdated} readOnly={readOnly} />;
return <EditCorrelationFormWrapper correlation={corr} onUpdated={onUpdated} readOnly={readOnly} />;
}
const getDatasourceCellStyles = (theme: GrafanaTheme2) => ({
@@ -1,12 +1,15 @@
import { css } from '@emotion/css';
import { useEffect } from 'react';
import { useCreateCorrelationMutation } from '@grafana/api-clients/rtkq/correlations/v0alpha1';
import { GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime';
import { PanelContainer, useStyles2 } from '@grafana/ui';
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
import { Wizard } from '../components/Wizard/Wizard';
import { useCorrelations } from '../useCorrelations';
import { generateAddSpec } from '../utils';
import { ConfigureCorrelationBasicInfoForm } from './ConfigureCorrelationBasicInfoForm';
import { ConfigureCorrelationSourceForm } from './ConfigureCorrelationSourceForm';
@@ -31,7 +34,55 @@ interface Props {
onCreated: () => void;
}
export const AddCorrelationForm = ({ onClose, onCreated }: Props) => {
export const AddCorrelationFormWrapper = ({ onClose, onCreated }: Props) => {
if (config.featureToggles.kubernetesCorrelations) {
return <AddCorrelationFormAppPlatform onClose={onClose} onCreated={onCreated} />;
}
return <AddCorrelationFormLegacy onClose={onClose} onCreated={onCreated} />;
};
export const AddCorrelationFormAppPlatform = ({ onClose, onCreated }: Props) => {
const styles = useStyles2(getStyles);
const [createCorrelation, { data, isLoading, isError }] = useCreateCorrelationMutation();
useEffect(() => {
if (!isError && !isLoading && data) {
onCreated();
}
}, [onCreated, isError, isLoading, data]);
const defaultValues: Partial<FormDTO> = { type: 'query', config: { target: {}, field: '' } };
const onSubmit = async (data: FormDTO) => {
const corrSpec = await generateAddSpec(data);
return createCorrelation({
correlation: {
metadata: {},
apiVersion: 'correlations.grafana.app/v0alpha1',
kind: 'Correlation',
spec: corrSpec,
},
});
};
return (
<PanelContainer className={styles.panelContainer}>
<CloseButton onClick={onClose} />
<CorrelationsFormContextProvider data={{ loading: isLoading, readOnly: false, correlation: undefined }}>
<Wizard<FormDTO>
defaultValues={defaultValues}
pages={[ConfigureCorrelationBasicInfoForm, ConfigureCorrelationTargetForm, ConfigureCorrelationSourceForm]}
navigation={CorrelationFormNavigation}
onSubmit={onSubmit}
/>
</CorrelationsFormContextProvider>
</PanelContainer>
);
};
export const AddCorrelationFormLegacy = ({ onClose, onCreated }: Props) => {
const styles = useStyles2(getStyles);
const {
@@ -1,8 +1,12 @@
import { useEffect } from 'react';
import { config } from '@grafana/runtime';
import { useUpdateCorrelationMutation } from 'app/api/clients/correlations/v0alpha1';
import { Wizard } from '../components/Wizard/Wizard';
import { Correlation } from '../types';
import { useCorrelations } from '../useCorrelations';
import { generatePartialEditSpec } from '../utils';
import { ConfigureCorrelationBasicInfoForm } from './ConfigureCorrelationBasicInfoForm';
import { ConfigureCorrelationSourceForm } from './ConfigureCorrelationSourceForm';
@@ -17,7 +21,15 @@ interface Props {
readOnly?: boolean;
}
export const EditCorrelationForm = ({ onUpdated, correlation, readOnly = false }: Props) => {
export const EditCorrelationFormWrapper = ({ onUpdated, correlation, readOnly = false }: Props) => {
if (config.featureToggles.kubernetesCorrelations) {
return <EditCorrelationFormAppPlatform onUpdated={onUpdated} correlation={correlation} readOnly={readOnly} />;
}
return <EditCorrelationFormLegacy onUpdated={onUpdated} correlation={correlation} readOnly={readOnly} />;
};
const EditCorrelationFormLegacy = ({ onUpdated, correlation, readOnly = false }: Props) => {
const {
update: { execute, loading, error, value },
} = useCorrelations();
@@ -43,3 +55,31 @@ export const EditCorrelationForm = ({ onUpdated, correlation, readOnly = false }
</CorrelationsFormContextProvider>
);
};
const EditCorrelationFormAppPlatform = ({ onUpdated, correlation, readOnly = false }: Props) => {
const [update, { isLoading, error, data }] = useUpdateCorrelationMutation();
// we use PATCH/update and build a partial spec here because PUT/replace requires us to specify
// the full app platform correlation including the api version and metadata,
// which we do not store in the frontend
const onSubmit = (data: EditFormDTO) => {
return update({ name: correlation.uid, patch: { spec: generatePartialEditSpec(data, correlation) } });
};
useEffect(() => {
if (!error && !isLoading && data) {
onUpdated();
}
}, [error, onUpdated, isLoading, data]);
return (
<CorrelationsFormContextProvider data={{ loading: isLoading, readOnly, correlation }}>
<Wizard<EditFormDTO>
defaultValues={correlation}
pages={[ConfigureCorrelationBasicInfoForm, ConfigureCorrelationTargetForm, ConfigureCorrelationSourceForm]}
onSubmit={readOnly ? (e) => () => {} : onSubmit}
navigation={CorrelationFormNavigation}
/>
</CorrelationsFormContextProvider>
);
};
@@ -9,7 +9,7 @@ import { OmitUnion } from '../types';
export interface FormExternalDTO {
sourceUID: string;
label: string;
description: string;
description?: string;
type: 'external';
config: CorrelationExternal['config'];
}
@@ -18,7 +18,7 @@ export interface FormQueryDTO {
sourceUID: string;
targetUID: string;
label: string;
description: string;
description?: string;
type: 'query';
config: CorrelationQuery['config'];
}
@@ -1,6 +1,9 @@
import { renderHook } from '@testing-library/react';
import { useListCorrelationQuery } from '@grafana/api-clients/rtkq/correlations/v0alpha1';
import { DataSourceRef } from '@grafana/schema/dist/esm/index';
import { toEnrichedCorrelationDataK8s } from './useCorrelationsK8s';
import { toEnrichedCorrelationDataK8s, useCorrelationsK8s } from './useCorrelationsK8s';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
@@ -15,69 +18,108 @@ jest.mock('@grafana/runtime', () => ({
}),
}));
describe('useCorrelationK8s.ts', () => {
jest.mock('@grafana/api-clients/rtkq/correlations/v0alpha1', () => ({
...jest.requireActual('@grafana/api-clients/rtkq/correlations/v0alpha1'),
useListCorrelationQuery: jest.fn(),
}));
const useListCorrelationMock = useListCorrelationQuery as jest.Mock;
describe('useCorrelationsK8s', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('toEnrichedCorrelationDataK8s', () => {
it('not finding a source ds should return undefined', () => {
it('returns undefined if the source datasource is not found', () => {
const correlation = toEnrichedCorrelationDataK8s({
apiVersion: 'test',
kind: 'test',
metadata: {},
apiVersion: 'testApiVer',
kind: 'testKind',
metadata: { name: 'testUid' },
spec: {
config: { field: 'test', target: {} },
label: 'test',
label: 'testLabel',
description: 'testDesc',
source: { group: 'notFoundGroup', name: 'notFoundUid' },
type: 'query',
type: 'external',
config: { field: 'testField', target: { url: 'testUrl' } },
},
});
expect(correlation).toBe(undefined);
});
it('should return a query correlation as expected', () => {
it('returns undefined if its a query correlation and the targetDS is not found', () => {
const correlation = toEnrichedCorrelationDataK8s({
metadata: { name: 'test' },
apiVersion: 'test',
kind: 'test',
apiVersion: 'testApiVer',
kind: 'testKind',
metadata: { name: 'testUid' },
spec: {
config: { field: 'test', target: { randomKey: 'randomValue' } },
label: 'test',
source: { group: 'foundGroup', name: 'foundName' },
target: { group: 'targetGroup', name: 'targetName' },
label: 'testLabel',
description: 'testDesc',
source: { group: 'notFoundGroup', name: 'foundUid' },
target: { group: 'notFoundGroup', name: 'notFoundUid' },
type: 'query',
config: { field: 'testField', target: { url: 'testUrl' } },
},
});
expect(correlation).toBe(undefined);
});
it('returns an external correlation', () => {
const correlation = toEnrichedCorrelationDataK8s({
apiVersion: 'testApiVer',
kind: 'testKind',
metadata: { name: 'testUid' },
spec: {
label: 'testLabel',
description: 'testDesc',
source: { group: 'notFoundGroup', name: 'foundUid' },
type: 'external',
config: { field: 'testField', target: { url: 'testUrl' } },
},
});
expect(correlation).toStrictEqual({
config: { field: 'test', target: { randomKey: 'randomValue' }, transformations: undefined },
description: undefined,
label: 'test',
config: { field: 'testField', target: { url: 'testUrl' }, transformations: undefined },
description: 'testDesc',
label: 'testLabel',
provisioned: false,
source: { type: 'foundName', uid: 'foundName' },
target: { type: 'targetName', uid: 'targetName' },
targetUID: 'targetName',
type: 'query',
uid: 'test',
source: { type: 'foundUid', uid: 'foundUid' },
type: 'external',
uid: 'testUid',
});
});
it('should return an external correlation as expected', () => {
it('returns a query correlation', () => {
const correlation = toEnrichedCorrelationDataK8s({
metadata: { name: 'test' },
apiVersion: 'test',
kind: 'test',
apiVersion: 'testApiVer',
kind: 'testKind',
metadata: { name: 'testUid' },
spec: {
config: { field: 'test', target: { url: 'testURL' } },
label: 'test',
source: { group: 'foundGroup', name: 'foundName' },
type: 'external',
label: 'testLabel',
description: 'testDesc',
source: { group: 'notFoundGroup', name: 'foundUid' },
target: { group: 'notFoundGroup', name: 'foundUid' },
type: 'query',
config: { field: 'testField', target: { url: 'testUrl' } },
},
});
expect(correlation).toStrictEqual({
config: { field: 'test', target: { url: 'testURL' }, transformations: undefined },
description: undefined,
label: 'test',
config: { field: 'testField', target: { url: 'testUrl' }, transformations: undefined },
description: 'testDesc',
label: 'testLabel',
provisioned: false,
source: { type: 'foundName', uid: 'foundName' },
type: 'external',
uid: 'test',
source: { type: 'foundUid', uid: 'foundUid' },
target: { type: 'foundUid', uid: 'foundUid' },
targetUID: 'foundUid',
type: 'query',
uid: 'testUid',
});
});
});
it('should pass the right limit based on page size', async () => {
useListCorrelationMock.mockReturnValue({ data: [] });
renderHook(() => useCorrelationsK8s(10, 5));
expect(useListCorrelationMock).toHaveBeenCalledWith({ limit: 50 });
});
});
+126 -1
View File
@@ -1,14 +1,27 @@
import { generatedAPI as correlationsAPIv0alpha1 } from '@grafana/api-clients/rtkq/correlations/v0alpha1';
import { DataFrame, DataFrameType, DataSourceInstanceSettings, FieldType, toDataFrame } from '@grafana/data';
import {
DataFrame,
DataFrameType,
DataSourceInstanceSettings,
FieldType,
SupportedTransformationType,
toDataFrame,
} from '@grafana/data';
import { config, CorrelationData } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema/dist/esm/index';
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
import { ExploreItemState } from 'app/types/explore';
import { EditFormDTO } from './Forms/types';
import { Correlation } from './types';
import { attachCorrelationsToDataFrames, generateDefaultLabel, generatePartialEditSpec } from './utils';
import * as utils from './utils';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getDataSourceSrv: jest.fn().mockReturnValue({
get: jest.fn().mockResolvedValue({
name: 'getTest',
getRef: () => {
return { type: 'testTypeFromLookup', uid: 'testUidFromLookup' };
},
@@ -70,6 +83,32 @@ describe('correlations utils', () => {
});
});
it('attaches external correlations defined in the configuration', () => {
config.featureToggles.lokiLogsDataplane = false;
const { testDataFrames, refIdMap, loki } = setup();
attachCorrelationsToDataFrames(
testDataFrames,
[
{
uid: 'loki-to-prometheus',
label: 'logs to metrics',
source: loki,
type: 'external',
config: { field: 'traceId', target: { url: 'testUrl' } },
provisioned: false,
},
],
refIdMap
);
expect(testDataFrames[0].fields[1].config.links).toHaveLength(1);
expect(testDataFrames[0].fields[1].config.links).toMatchObject([
{
title: 'logs to metrics',
},
]);
});
it('does not create duplicates when attaching links to the same data frame', () => {
const { testDataFrames, correlations, refIdMap } = setup();
utils.attachCorrelationsToDataFrames(testDataFrames, correlations, refIdMap);
@@ -102,6 +141,92 @@ describe('correlations utils', () => {
config.featureToggles.lokiLogsDataplane = originalDataplaneState;
});
it('generates a partial spec with config only when nothing is edited', () => {
const correlation: Correlation = {
uid: 'test',
sourceUID: 'test',
label: 'test',
provisioned: false,
type: 'external',
config: { field: 'test', target: { url: 'test' } },
};
const editForm: EditFormDTO = { ...correlation, label: correlation.label! };
const partialSpec = generatePartialEditSpec(editForm, correlation);
expect(partialSpec).toStrictEqual({ config: { field: 'test', target: { url: 'test' } } });
});
it('generates a partial spec as expected when things are edited', () => {
const correlation: Correlation = {
uid: 'test',
sourceUID: 'test',
label: 'test',
provisioned: false,
type: 'external',
config: { field: 'test', target: { url: 'test' } },
};
const editForm: EditFormDTO = {
...correlation,
label: 'diffLabel',
description: 'diffDesc',
type: 'query',
config: {
field: 'diffField',
target: { diff: 'target' },
transformations: [
{
type: SupportedTransformationType.Logfmt,
expression: 'diffExp',
mapValue: 'diffMapValue',
field: 'diffField',
},
],
},
};
const partialSpec = generatePartialEditSpec(editForm, correlation);
expect(partialSpec).toStrictEqual({
label: 'diffLabel',
description: 'diffDesc',
type: 'query',
config: {
field: 'diffField',
target: { diff: 'target' },
transformations: [{ expression: 'diffExp', field: 'diffField', mapValue: 'diffMapValue', type: 'logfmt' }],
},
});
});
it('generates the expected label from pane datasource when not mixed', async () => {
const queries: DataQuery[] = [{ refId: 'A', datasource: { uid: 'testQuery' } }];
const sourcePane: ExploreItemState = {
datasourceInstance: { name: 'testA', meta: { mixed: false } },
queries: queries,
queryKeys: [],
} as unknown as ExploreItemState;
const targetPane: ExploreItemState = {
datasourceInstance: { name: 'testB', meta: { mixed: false } },
queries: queries,
queryKeys: [],
} as unknown as ExploreItemState;
const label = await generateDefaultLabel(sourcePane, targetPane);
expect(label).toBe('testA to testB');
});
it('generates the expected label from query datasources when mixed', async () => {
const queriesA: DataQuery[] = [{ refId: 'A', datasource: { uid: 'testQueryA' } }];
const queriesB: DataQuery[] = [{ refId: 'B', datasource: { uid: 'testQueryB' } }];
const sourcePane: ExploreItemState = {
datasourceInstance: { name: 'testA', meta: { mixed: true } },
queries: queriesA,
queryKeys: [],
} as unknown as ExploreItemState;
const targetPane: ExploreItemState = {
datasourceInstance: { name: 'testB', meta: { mixed: false } },
queries: queriesB,
queryKeys: [],
} as unknown as ExploreItemState;
const label = await generateDefaultLabel(sourcePane, targetPane);
expect(label).toBe('getTest to testB');
});
describe('getCorrelationsFromStorage', () => {
const originalFeatureToggles = config.featureToggles;
+55 -2
View File
@@ -1,6 +1,10 @@
import { isEqual } from 'lodash';
import { lastValueFrom } from 'rxjs';
import { generatedAPI as correlationsAPIv0alpha1 } from '@grafana/api-clients/rtkq/correlations/v0alpha1';
import {
generatedAPI as correlationsAPIv0alpha1,
CorrelationSpec,
} from '@grafana/api-clients/rtkq/correlations/v0alpha1';
import { DataFrame, DataLinkConfigOrigin } from '@grafana/data';
import {
config,
@@ -19,7 +23,8 @@ import { formatValueName } from '../explore/PrometheusListView/ItemLabels';
import { getDatasourceUIDs } from '../explore/state/utils';
import { parseLogsFrame } from '../logs/logsFrame';
import { CreateCorrelationParams, CreateCorrelationResponse } from './types';
import { EditFormDTO, FormDTO } from './Forms/types';
import { Correlation, CreateCorrelationParams, CreateCorrelationResponse } from './types';
import { CorrelationsResponse, getData, toEnrichedCorrelationsData } from './useCorrelations';
import { toEnrichedCorrelationDataK8s } from './useCorrelationsK8s';
@@ -150,6 +155,54 @@ export const generateDefaultLabel = async (sourcePane: ExploreItemState, targetP
});
};
export const generatePartialEditSpec = (data: EditFormDTO, correlation: Correlation): Partial<CorrelationSpec> => {
let partialSpec: Partial<CorrelationSpec> = {};
if (data.label !== correlation.label) {
partialSpec.label = data.label;
}
if (data.description !== correlation.description) {
partialSpec.description = data.description;
}
if (data.type !== correlation.type) {
partialSpec.type = data.type;
}
// target is only loosely defined as an object, so always copy it
partialSpec.config = { field: data.config.field, target: data.config.target };
if (
data.config.transformations !== undefined &&
!isEqual(data.config.transformations, correlation.config.transformations)
) {
partialSpec.config.transformations = data.config.transformations.map((t) => {
return { expression: t.expression, field: t.field, mapValue: t.mapValue, type: t.type };
});
}
return partialSpec;
};
export const generateAddSpec = async (data: FormDTO): Promise<CorrelationSpec> => {
const dsSrv = getDataSourceSrv();
const sourceDs = await dsSrv.get(data.sourceUID);
let targetDs;
if ('targetUID' in data) {
targetDs = await dsSrv.get(data.targetUID!);
}
return {
label: data.label,
description: data.description,
source: { group: sourceDs.type, name: sourceDs.uid },
target: targetDs?.uid !== undefined ? { group: targetDs.type, name: targetDs?.uid } : undefined,
type: data.type,
config: {
field: data.config.field,
target: { ...data.config.target },
transformations: data.config.transformations,
},
};
};
export const correlationsLogger = createMonitoringLogger('features.correlations');
// legacy just needs uid for lookup, remote storage needs name/group
+4
View File
@@ -4853,6 +4853,10 @@
"view-json-diff": "View JSON diff to see all changes"
}
},
"correlation": {
"edit-error": "Error updating correlation",
"edit-success": "Correlation updated"
},
"correlations": {
"add-new": "Add new",
"alert": {