mirror of
https://github.com/grafana/grafana.git
synced 2026-08-08 20:28:14 -05:00
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:
@@ -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
@@ -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)
|
||||
)
|
||||
|
||||
+3
-6
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
+3
-3
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user