Correlations: Migrate Correlations list and correlations delete to use app platform (#113803)

* WIP

* export library from package, use new location

* wip……

* fix bad merge

* finish useCorrelations list

* add more clarifying comments

* start create correlation, attempt to fix target type

* Try to get targetspec change to stick

* fully bring in the type for target

* fix test that is technically correct but not great

* split

* paging

* paging

* WIP

* Add this to show what I’m trying to access

* properly split logic

* move conversion logic to hook, remove unneeded memo

* add fake pagination

* WIP

* wip

* fix deletion

* cleanup

* change limit

* fix the error type

* fix linter errors

* add wrapper test, WIP main test but remove the errors

* Change test to only use legacy version for now

* Testing the test

* fix lint suppressions

* fix tests

* fix lint/fmt

* Don’t crash if bad data, fix pagination to be usable with cursor only

* Add tests

* move back to reasonable number

* 🧹

* Fix based on PR feedback

* do ds lookup instead of trusting the uid

---------

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
Kristina 2026-01-20 10:59:18 -06:00 committed by GitHub
parent 6e216b2af2
commit 2d9ec64be0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 370 additions and 63 deletions

View file

@ -1,10 +1,18 @@
include ../sdk.mk
.PHONY: generate # Run Grafana App SDK code generation
generate: install-app-sdk update-app-sdk
generate: do-generate post-generate-cleanup
.PHONY: do-generate
do-generate: install-app-sdk update-app-sdk
@$(APP_SDK_BIN) generate \
--source=./kinds/ \
--gogenpath=./pkg/apis \
--grouping=group \
--genoperatorstate=false \
--defencoding=none
--defencoding=none
.PHONY: post-generate-cleanup
post-generate-cleanup: ## Fix TargetSpec OpenAPI schema
# Fix the TargetSpec schema in manifest - remove nested additionalProperties
@sed -i.bak 's|"TargetSpec":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"}|"TargetSpec":{"additionalProperties":{},"type":"object"}|g' ./pkg/apis/correlation_manifest.go && rm ./pkg/apis/correlation_manifest.go.bak

View file

@ -31,7 +31,9 @@ ConfigSpec: {
transformations?: [...TransformationSpec]
}
TargetSpec: [string]: _
TargetSpec: {
...
}
TransformationSpec: {
type: "regex" | "logfmt"

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":{"additionalProperties":{},"type":"object"},"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":{},"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"}}`)
versionSchemaCorrelationv0alpha1 app.VersionSchema
_ = json.Unmarshal(rawSchemaCorrelationv0alpha1, &versionSchemaCorrelationv0alpha1)
)

View file

@ -1763,11 +1763,6 @@
"count": 2
}
},
"public/app/features/correlations/CorrelationsPage.tsx": {
"no-restricted-syntax": {
"count": 1
}
},
"public/app/features/correlations/Forms/ConfigureCorrelationBasicInfoForm.tsx": {
"no-restricted-syntax": {
"count": 2

View file

@ -408,9 +408,7 @@ export type ObjectMeta = {
uid?: string;
};
export type CorrelationTargetSpec = {
[key: string]: {
[key: string]: any;
};
[key: string]: any;
};
export type CorrelationTransformationSpec = {
expression: string;

View file

@ -16,4 +16,16 @@ describe('Pagination component', () => {
expect(screen.getAllByRole('button')).toHaveLength(9);
expect(screen.getAllByTestId('pagination-ellipsis-icon')).toHaveLength(2);
});
it('should only render the page number if number of pages is 0', () => {
render(<Pagination currentPage={8} numberOfPages={0} onNavigate={() => {}} />);
expect(screen.getAllByRole('button')).toHaveLength(2);
expect(screen.getAllByRole('button')[1]).toBeEnabled();
expect(screen.getByText('8')).toBeVisible();
});
it('should disable the next page button if hasNextPage is false', () => {
render(<Pagination currentPage={8} numberOfPages={0} onNavigate={() => {}} hasNextPage={false} />);
expect(screen.getAllByRole('button')).toHaveLength(2);
expect(screen.getAllByRole('button')[0]).toBeEnabled();
expect(screen.getAllByRole('button')[1]).toBeDisabled();
});
});

View file

@ -19,6 +19,8 @@ export interface Props {
/** Small version only shows the current page and the navigation buttons. */
showSmallVersion?: boolean;
className?: string;
/** If we are using cursor based pagination, disable next page button when we have no cursor */
hasNextPage?: boolean;
}
/**
@ -33,6 +35,7 @@ export const Pagination = ({
hideWhenSinglePage,
showSmallVersion,
className,
hasNextPage,
}: Props) => {
const styles = useStyles2(getStyles);
const pageLengthToCondense = showSmallVersion ? 1 : 8;
@ -122,13 +125,14 @@ export const Pagination = ({
</Button>
</li>
{pageButtons}
{pageButtons.length === 0 && <li className={styles.item}>{currentPage}</li>}
<li className={styles.item}>
<Button
aria-label={nextPageLabel}
size="sm"
variant="secondary"
onClick={() => onNavigate(currentPage + 1)}
disabled={currentPage === numberOfPages}
disabled={hasNextPage === false || currentPage === numberOfPages}
>
<Icon name="angle-right" />
</Button>

View file

@ -2,7 +2,9 @@ package correlations
import (
"context"
b64 "encoding/base64"
"fmt"
"strconv"
"strings"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
@ -78,27 +80,53 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO
}
}
if options.Continue != "" {
return nil, fmt.Errorf("paging not yet supported")
page := int64(0)
limit := int64(100000)
if options != nil {
if options.Limit > 0 {
limit = options.Limit
}
if options.Continue != "" {
token, err := decodeContinueToken(options.Continue)
if err != nil {
return nil, err
}
if token.Limit != limit {
return nil, fmt.Errorf("continue token limit does not match the previous request")
}
page = token.Page
}
}
rsp, err := s.service.GetCorrelations(ctx, correlations.GetCorrelationsQuery{
OrgId: orgID,
Limit: 1000,
Limit: limit + 1, // the plus one indicates we have reached the limit
Page: page,
SourceUIDs: uids,
})
if err != nil {
return nil, err
}
list := &correlationsV0.CorrelationList{
Items: make([]correlationsV0.Correlation, len(rsp.Correlations)),
Items: make([]correlationsV0.Correlation, 0, len(rsp.Correlations)),
}
for i, orig := range rsp.Correlations {
if i >= int(limit) {
remaining := rsp.TotalCount - (page * limit) - int64(len(list.Items))
if remaining > 0 {
list.RemainingItemCount = &remaining
}
list.Continue = encodeContinueToken(page+1, limit)
break
}
c, err := correlations.ToResource(orig, s.namespacer)
if err != nil {
return nil, err
}
list.Items[i] = *c
list.Items = append(list.Items, *c)
}
return list, nil
}
@ -193,3 +221,33 @@ func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidatio
func (s *legacyStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) {
return nil, fmt.Errorf("DeleteCollection for shorturl not implemented")
}
type continueToken struct {
Page int64
Limit int64
}
func encodeContinueToken(page, limit int64) string {
data := fmt.Sprintf("%d/%d", page, limit)
return b64.StdEncoding.EncodeToString([]byte(data)) // use base64 so it is not treated like query params
}
func decodeContinueToken(s string) (token continueToken, err error) {
decoded, err := b64.StdEncoding.DecodeString(s)
if err != nil {
return token, fmt.Errorf("invalid continue token")
}
parts := strings.Split(string(decoded), "/")
if len(parts) != 2 {
return token, fmt.Errorf("invalid continue token")
}
token.Page, err = strconv.ParseInt(parts[0], 10, 64)
if err != nil {
return token, fmt.Errorf("invalid continue token (page)")
}
token.Limit, err = strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return token, fmt.Errorf("invalid continue token")
}
return token, nil
}

View file

@ -948,10 +948,7 @@
},
"com.github.grafana.grafana.apps.correlations.pkg.apis.correlation.v0alpha1.CorrelationTargetSpec": {
"type": "object",
"additionalProperties": {
"type": "object",
"additionalProperties": {}
}
"additionalProperties": {}
},
"com.github.grafana.grafana.apps.correlations.pkg.apis.correlation.v0alpha1.CorrelationTransformationSpec": {
"type": "object",

View file

@ -23,7 +23,7 @@ import { configureStore } from 'app/store/configureStore';
import { mockDataSource } from '../alerting/unified/mocks';
import CorrelationsPage from './CorrelationsPage';
import { CorrelationsPageLegacy } from './CorrelationsPageWrapper';
import {
createCreateCorrelationResponse,
createFetchCorrelationsError,
@ -112,7 +112,7 @@ const renderWithContext = async (
const renderResult = render(
<TestProvider store={configureStore({})} grafanaContext={grafanaContext}>
<CorrelationsPage />
<CorrelationsPageLegacy />
</TestProvider>,
{
queries: {

View file

@ -2,9 +2,10 @@ import { css } from '@emotion/css';
import { negate } from 'lodash';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Status } from '@grafana/api-clients/rtkq/correlations/v0alpha1';
import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { CorrelationData, isFetchError, reportInteraction } from '@grafana/runtime';
import { CorrelationData, CorrelationsData, FetchError, isFetchError, reportInteraction } from '@grafana/runtime';
import {
Badge,
Button,
@ -27,11 +28,27 @@ import { AccessControlAction } from 'app/types/accessControl';
import { AddCorrelationForm } from './Forms/AddCorrelationForm';
import { EditCorrelationForm } from './Forms/EditCorrelationForm';
import { EmptyCorrelationsCTA } from './components/EmptyCorrelationsCTA';
import type { Correlation, RemoveCorrelationParams } from './types';
import { useCorrelations } from './useCorrelations';
import type { Correlation, GetCorrelationsParams, RemoveCorrelationParams } from './types';
type CorrelationsPageProps = {
fetchCorrelations: (params: GetCorrelationsParams) => Promise<CorrelationsData> | CorrelationsData;
correlations?: CorrelationsData;
isLoading: boolean;
changePageFn?: (page: number) => void;
removeFn?: (params: RemoveCorrelationParams) => Promise<
| {
message: string;
}
| Status
>;
error?: Error | FetchError;
hasNextPage?: boolean;
};
const collator = new Intl.Collator();
const sortDatasource: SortByFn<CorrelationData> = (a, b, column) =>
a.values[column].name.localeCompare(b.values[column].name);
collator.compare(a.values[column].name, b.values[column].name);
const isCorrelationsReadOnly = (correlation: CorrelationData) => correlation.provisioned;
@ -40,7 +57,8 @@ const loaderWrapper = css({
justifyContent: 'center',
});
export default function CorrelationsPage() {
export default function CorrelationsPage(props: CorrelationsPageProps) {
const { fetchCorrelations, correlations, isLoading, error, removeFn, changePageFn, hasNextPage } = props;
const navModel = useNavModel('correlations');
const [isAdding, setIsAddingValue] = useState(false);
const page = useRef(1);
@ -52,11 +70,6 @@ export default function CorrelationsPage() {
}
};
const {
remove,
get: { execute: fetchCorrelations, ...get },
} = useCorrelations();
const canWriteCorrelations = contextSrv.hasPermission(AccessControlAction.DataSourcesWrite);
const handleAdded = useCallback(() => {
@ -72,15 +85,17 @@ export default function CorrelationsPage() {
const handleDelete = useCallback(
async (params: RemoveCorrelationParams, isLastRow: boolean) => {
await remove.execute(params);
reportInteraction('grafana_correlations_deleted');
if (removeFn) {
await removeFn(params);
reportInteraction('grafana_correlations_deleted');
if (isLastRow) {
page.current--;
if (isLastRow) {
page.current--;
}
fetchCorrelations({ page: page.current });
}
fetchCorrelations({ page: page.current });
},
[remove, fetchCorrelations]
[removeFn, fetchCorrelations]
);
useEffect(() => {
@ -102,9 +117,7 @@ export default function CorrelationsPage() {
!provisioned && (
<DeleteButton
aria-label={t('correlations.list.delete', 'delete correlation')}
onConfirm={() =>
handleDelete({ sourceUID, uid }, page.current > 1 && index === 0 && data?.correlations.length === 1)
}
onConfirm={() => handleDelete({ sourceUID, uid }, page.current > 1 && index === 0 && corrData.length === 1)}
closeOnConfirm
/>
)
@ -145,9 +158,9 @@ export default function CorrelationsPage() {
[RowActions, canWriteCorrelations]
);
const data = useMemo(() => get.value, [get.value]);
const showEmptyListCTA = data?.correlations.length === 0 && !isAdding && !get.error;
const addButton = canWriteCorrelations && data?.correlations?.length !== 0 && data !== undefined && !isAdding && (
const corrData = correlations?.correlations ?? [];
const showEmptyListCTA = corrData.length === 0 && !isAdding && !error;
const addButton = canWriteCorrelations && corrData.length !== 0 && !isAdding && (
<Button icon="plus" onClick={() => setIsAdding(true)}>
<Trans i18nKey="correlations.add-new">Add new</Trans>
</Button>
@ -170,25 +183,23 @@ export default function CorrelationsPage() {
>
<Page.Contents>
<div>
{!data && get.loading && (
{isLoading && (
<div className={loaderWrapper}>
<LoadingPlaceholder text={t('correlations.list.loading', 'loading...')} />
</div>
)}
{showEmptyListCTA && (
<EmptyCorrelationsCTA canWriteCorrelations={canWriteCorrelations} onClick={() => setIsAdding(true)} />
)}
{
// This error is not actionable, it'd be nice to have a recovery button
get.error && (
error && (
<Alert
severity="error"
title={t('correlations.alert.title', 'Error fetching correlation data')}
topSpacing={2}
>
{(isFetchError(get.error) && get.error.data?.message) ||
{(isFetchError(error) && error.data?.message) ||
t(
'correlations.alert.error-message',
'An unknown error occurred while fetching correlation data. Please try again.'
@ -196,10 +207,9 @@ export default function CorrelationsPage() {
</Alert>
)
}
{isAdding && <AddCorrelationForm onClose={() => setIsAdding(false)} onCreated={handleAdded} />}
{data && data.correlations.length >= 1 && (
{correlations && corrData.length >= 1 && (
<>
<InteractiveTable
renderExpandedRow={(correlation) => (
@ -210,15 +220,19 @@ export default function CorrelationsPage() {
/>
)}
columns={columns}
data={data.correlations}
data={corrData}
getRowId={(correlation) => `${correlation.source.uid}-${correlation.uid}`}
/>
<Pagination
currentPage={page.current}
numberOfPages={Math.ceil(data.totalCount / data.limit)}
numberOfPages={Math.ceil(correlations?.totalCount / correlations?.limit)}
onNavigate={(toPage: number) => {
if (changePageFn) {
changePageFn(toPage);
}
fetchCorrelations({ page: (page.current = toPage) });
}}
hasNextPage={hasNextPage}
/>
</>
)}

View file

@ -0,0 +1,54 @@
import { render } from 'test/test-utils';
import { config } from '@grafana/runtime';
import CorrelationsPageWrapper from './CorrelationsPageWrapper';
jest.mock('app/core/services/context_srv');
const mockUseCorrelations = jest.fn().mockReturnValue({
remove: { execute: jest.fn() },
get: { execute: jest.fn(), value: [], loading: false, error: undefined },
});
const mockUseCorrelationsK8s = jest.fn().mockReturnValue({
currentData: [],
isLoading: false,
error: undefined,
remainingItems: 0,
});
jest.mock('./useCorrelations', () => ({
useCorrelations: () => mockUseCorrelations(),
}));
jest.mock('./useCorrelationsK8s', () => ({
useCorrelationsK8s: () => mockUseCorrelationsK8s(),
}));
describe('CorrelationsPageWrapper', () => {
const originalFeatureToggles = config.featureToggles;
beforeEach(() => {
jest.clearAllMocks();
});
afterEach(() => {
config.featureToggles = originalFeatureToggles;
});
describe('with the kubernetes feature toggle on', () => {
it('uses the K8s correlations hook', () => {
config.featureToggles = { ...originalFeatureToggles, kubernetesCorrelations: true };
render(<CorrelationsPageWrapper />);
expect(mockUseCorrelationsK8s).toHaveBeenCalled();
});
});
describe('with the kubernetes feature toggle off', () => {
it('uses the legacy correlations hook', () => {
config.featureToggles = { ...originalFeatureToggles, kubernetesCorrelations: false };
render(<CorrelationsPageWrapper />);
expect(mockUseCorrelations).toHaveBeenCalled();
});
});
});

View file

@ -0,0 +1,67 @@
import { useState } from 'react';
import { handleRequestError } from '@grafana/api-clients';
import { useDeleteCorrelationMutation } from '@grafana/api-clients/rtkq/correlations/v0alpha1';
import { config } from '@grafana/runtime';
import CorrelationsPage from './CorrelationsPage';
import { GetCorrelationsParams, RemoveCorrelationParams } from './types';
import { useCorrelations } from './useCorrelations';
import { useCorrelationsK8s } from './useCorrelationsK8s';
export function CorrelationsPageLegacy() {
const { remove, get } = useCorrelations();
return (
<CorrelationsPage
fetchCorrelations={get.execute}
correlations={get.value}
isLoading={get.loading}
error={get.error}
removeFn={remove.execute}
/>
);
}
function CorrelationsPageAppPlatform() {
const [page, setPage] = useState(1);
const limit = 100;
const { currentData, isLoading, error, doesContinue } = useCorrelationsK8s(limit, page);
const [deleteCorrelation] = useDeleteCorrelationMutation();
// we cant do a straight refetch, we have to pass in new pages if necessary
const enhRefetch = (params: GetCorrelationsParams) => {
return { correlations: currentData, page: params.page, limit, totalCount: 0 };
};
const fmtedError = error ? handleRequestError(error) : undefined;
return (
<CorrelationsPage
fetchCorrelations={enhRefetch}
changePageFn={(toPage) => {
setPage(toPage);
}}
correlations={{
correlations: currentData,
page: 0,
limit: limit,
totalCount: 0,
}}
isLoading={isLoading}
error={fmtedError?.error}
removeFn={(params: RemoveCorrelationParams) => {
const deleteData = deleteCorrelation({ name: params.uid });
return deleteData.unwrap();
}}
hasNextPage={doesContinue}
/>
);
}
export default function CorrelationsPageWrapper() {
if (config.featureToggles.kubernetesCorrelations) {
return <CorrelationsPageAppPlatform />;
}
return <CorrelationsPageLegacy />;
}

View file

@ -23,7 +23,7 @@ export interface CorrelationsResponse {
totalCount: number;
}
const toEnrichedCorrelationData = ({ sourceUID, ...correlation }: Correlation): CorrelationData | undefined => {
export const toEnrichedCorrelationData = ({ sourceUID, ...correlation }: Correlation): CorrelationData | undefined => {
const sourceDatasource = getDataSourceSrv().getInstanceSettings(sourceUID);
const targetDatasource =
correlation.type === 'query' ? getDataSourceSrv().getInstanceSettings(correlation.targetUID) : undefined;
@ -90,8 +90,8 @@ export const useCorrelations = () => {
const { backend } = useGrafana();
const [getInfo, get] = useAsyncFn<(params: GetCorrelationsParams) => Promise<CorrelationsData>>(
(params) =>
lastValueFrom(
async (params) => {
return lastValueFrom(
backend.fetch<CorrelationsResponse>({
url: '/api/datasources/correlations',
params: { page: params.page },
@ -100,13 +100,15 @@ export const useCorrelations = () => {
})
)
.then(getData)
.then(toEnrichedCorrelationsData),
.then(toEnrichedCorrelationsData);
},
[backend]
);
const [createInfo, create] = useAsyncFn<(params: CreateCorrelationParams) => Promise<CorrelationData>>(
({ sourceUID, ...correlation }) =>
backend
async ({ sourceUID, ...correlation }) => {
return backend
.post<CreateCorrelationResponse>(`/api/datasources/uid/${sourceUID}/correlations`, correlation)
.then((response) => {
const enrichedCorrelation = toEnrichedCorrelationData(response.result);
@ -115,7 +117,8 @@ export const useCorrelations = () => {
} else {
throw new Error('invalid sourceUID');
}
}),
});
},
[backend]
);

View file

@ -0,0 +1,94 @@
import { handleRequestError } from '@grafana/api-clients';
import {
Correlation as CorrelationK8s,
useListCorrelationQuery,
} from '@grafana/api-clients/rtkq/correlations/v0alpha1';
import { SupportedTransformationType } from '@grafana/data';
import { CorrelationData, CorrelationExternal, CorrelationQuery, getDataSourceSrv } from '@grafana/runtime';
import { toEnrichedCorrelationData } from './useCorrelations';
export const toEnrichedCorrelationDataK8s = (item: CorrelationK8s): CorrelationData | undefined => {
const dsSrv = getDataSourceSrv();
const sourceDS = dsSrv.getInstanceSettings({ type: item.spec.source.group, uid: item.spec.source.name });
if (sourceDS !== undefined) {
const baseCor = {
uid: item.metadata.name!,
sourceUID: sourceDS.uid,
label: item.spec.label,
description: item.spec.description,
provisioned: false, // todo
};
const transformationsFmt = item.spec.config.transformations?.map((trans) => {
return {
...trans,
type: trans.type === 'regex' ? SupportedTransformationType.Regex : SupportedTransformationType.Logfmt,
};
});
if (item.spec.type === 'external') {
const extCorr: CorrelationExternal = {
...baseCor,
type: 'external',
config: {
field: item.spec.config.field,
target: {
url: item.spec.config?.target?.url || '',
},
transformations: transformationsFmt,
},
};
return toEnrichedCorrelationData(extCorr);
} else {
const targetDs = dsSrv.getInstanceSettings({ type: item.spec.target?.group, uid: item.spec.target?.name });
if (targetDs !== undefined) {
const queryCorr: CorrelationQuery = {
...baseCor,
type: 'query',
targetUID: targetDs.uid,
config: {
field: item.spec.config.field,
target: item.spec.config.target,
transformations: transformationsFmt,
},
};
return toEnrichedCorrelationData(queryCorr);
} else {
return undefined;
}
}
} else {
return undefined;
}
};
// we're faking traditional pagination here, realistically folks shouldnt have enough correlations to see a performance impact but if they do we can change the ui
export const useCorrelationsK8s = (limit = 100, page: number) => {
let pagedLimit = limit;
if (page > 1) {
pagedLimit = limit * page;
}
const { currentData, isLoading, error } = useListCorrelationQuery({ limit: pagedLimit });
const startIdx = limit * (page - 1);
const pagedData = currentData?.items.slice(startIdx, startIdx + limit) ?? [];
const enrichedCorrelations =
currentData !== undefined
? pagedData
.filter((i) => i.metadata.name !== undefined)
.map((item) => toEnrichedCorrelationDataK8s(item))
.filter((i) => i !== undefined)
: [];
const fmtedError = error ? handleRequestError(error) : undefined;
return {
currentData: enrichedCorrelations,
isLoading,
error: fmtedError,
remainingItems: currentData?.metadata.remainingItemCount || 0,
doesContinue: currentData?.metadata.continue !== undefined,
};
};

View file

@ -140,7 +140,8 @@ export function getAppRoutes(): RouteDescriptor[] {
{
path: '/datasources/correlations',
component: SafeDynamicImport(
() => import(/* webpackChunkName: "CorrelationsPage" */ 'app/features/correlations/CorrelationsPage')
() =>
import(/* webpackChunkName: "CorrelationsPageWrapper" */ 'app/features/correlations/CorrelationsPageWrapper')
),
},
{