From 2d9ec64be048ea3067cd79909d8ce914bfee25da Mon Sep 17 00:00:00 2001 From: Kristina Date: Tue, 20 Jan 2026 10:59:18 -0600 Subject: [PATCH] Correlations: Migrate Correlations list and correlations delete to use app platform (#113803) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- apps/correlations/Makefile | 12 ++- apps/correlations/kinds/correlations.cue | 4 +- .../pkg/apis/correlation_manifest.go | 2 +- eslint-suppressions.json | 5 - .../correlations/v0alpha1/endpoints.gen.ts | 4 +- .../components/Pagination/Pagination.test.tsx | 12 +++ .../src/components/Pagination/Pagination.tsx | 6 +- .../apps/correlations/legacy_storage.go | 68 +++++++++++++- .../correlations.grafana.app-v0alpha1.json | 5 +- .../correlations/CorrelationsPage.test.tsx | 4 +- .../correlations/CorrelationsPage.tsx | 76 +++++++++------ .../CorrelationsPageWrapper.test.tsx | 54 +++++++++++ .../correlations/CorrelationsPageWrapper.tsx | 67 +++++++++++++ .../features/correlations/useCorrelations.ts | 17 ++-- .../correlations/useCorrelationsK8s.ts | 94 +++++++++++++++++++ public/app/routes/routes.tsx | 3 +- 16 files changed, 370 insertions(+), 63 deletions(-) create mode 100644 public/app/features/correlations/CorrelationsPageWrapper.test.tsx create mode 100644 public/app/features/correlations/CorrelationsPageWrapper.tsx create mode 100644 public/app/features/correlations/useCorrelationsK8s.ts diff --git a/apps/correlations/Makefile b/apps/correlations/Makefile index bc8d6d30cb5..6e8e4df180b 100644 --- a/apps/correlations/Makefile +++ b/apps/correlations/Makefile @@ -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 \ No newline at end of file + --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 \ No newline at end of file diff --git a/apps/correlations/kinds/correlations.cue b/apps/correlations/kinds/correlations.cue index 4089057859f..192c1b9568e 100644 --- a/apps/correlations/kinds/correlations.cue +++ b/apps/correlations/kinds/correlations.cue @@ -31,7 +31,9 @@ ConfigSpec: { transformations?: [...TransformationSpec] } -TargetSpec: [string]: _ +TargetSpec: { + ... +} TransformationSpec: { type: "regex" | "logfmt" diff --git a/apps/correlations/pkg/apis/correlation_manifest.go b/apps/correlations/pkg/apis/correlation_manifest.go index b1ba6356786..70fd8430bb4 100644 --- a/apps/correlations/pkg/apis/correlation_manifest.go +++ b/apps/correlations/pkg/apis/correlation_manifest.go @@ -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) ) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index e5a73baf213..34f7d8d4766 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -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 diff --git a/packages/grafana-api-clients/src/clients/rtkq/correlations/v0alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/correlations/v0alpha1/endpoints.gen.ts index 7fa1043fd5c..f2343b137f1 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/correlations/v0alpha1/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/correlations/v0alpha1/endpoints.gen.ts @@ -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; diff --git a/packages/grafana-ui/src/components/Pagination/Pagination.test.tsx b/packages/grafana-ui/src/components/Pagination/Pagination.test.tsx index a0bfec245cf..721782e18c3 100644 --- a/packages/grafana-ui/src/components/Pagination/Pagination.test.tsx +++ b/packages/grafana-ui/src/components/Pagination/Pagination.test.tsx @@ -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( {}} />); + 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( {}} hasNextPage={false} />); + expect(screen.getAllByRole('button')).toHaveLength(2); + expect(screen.getAllByRole('button')[0]).toBeEnabled(); + expect(screen.getAllByRole('button')[1]).toBeDisabled(); + }); }); diff --git a/packages/grafana-ui/src/components/Pagination/Pagination.tsx b/packages/grafana-ui/src/components/Pagination/Pagination.tsx index 57afc826b93..d5aa5018159 100644 --- a/packages/grafana-ui/src/components/Pagination/Pagination.tsx +++ b/packages/grafana-ui/src/components/Pagination/Pagination.tsx @@ -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 = ({ {pageButtons} + {pageButtons.length === 0 &&
  • {currentPage}
  • }
  • diff --git a/pkg/registry/apps/correlations/legacy_storage.go b/pkg/registry/apps/correlations/legacy_storage.go index 733ad4ec330..23967e12807 100644 --- a/pkg/registry/apps/correlations/legacy_storage.go +++ b/pkg/registry/apps/correlations/legacy_storage.go @@ -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 +} diff --git a/pkg/tests/apis/openapi_snapshots/correlations.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/correlations.grafana.app-v0alpha1.json index ae133b13d34..0f8b1e2b7f1 100644 --- a/pkg/tests/apis/openapi_snapshots/correlations.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/correlations.grafana.app-v0alpha1.json @@ -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", diff --git a/public/app/features/correlations/CorrelationsPage.test.tsx b/public/app/features/correlations/CorrelationsPage.test.tsx index f8938a14a94..0417feffc85 100644 --- a/public/app/features/correlations/CorrelationsPage.test.tsx +++ b/public/app/features/correlations/CorrelationsPage.test.tsx @@ -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( - + , { queries: { diff --git a/public/app/features/correlations/CorrelationsPage.tsx b/public/app/features/correlations/CorrelationsPage.tsx index 459f3279053..1a0568a4540 100644 --- a/public/app/features/correlations/CorrelationsPage.tsx +++ b/public/app/features/correlations/CorrelationsPage.tsx @@ -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; + 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 = (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 && ( - 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 && ( @@ -170,25 +183,23 @@ export default function CorrelationsPage() { >
    - {!data && get.loading && ( + {isLoading && (
    )} - {showEmptyListCTA && ( setIsAdding(true)} /> )} - { // This error is not actionable, it'd be nice to have a recovery button - get.error && ( + error && ( - {(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() { ) } - {isAdding && setIsAdding(false)} onCreated={handleAdded} />} - {data && data.correlations.length >= 1 && ( + {correlations && corrData.length >= 1 && ( <> ( @@ -210,15 +220,19 @@ export default function CorrelationsPage() { /> )} columns={columns} - data={data.correlations} + data={corrData} getRowId={(correlation) => `${correlation.source.uid}-${correlation.uid}`} /> { + if (changePageFn) { + changePageFn(toPage); + } fetchCorrelations({ page: (page.current = toPage) }); }} + hasNextPage={hasNextPage} /> )} diff --git a/public/app/features/correlations/CorrelationsPageWrapper.test.tsx b/public/app/features/correlations/CorrelationsPageWrapper.test.tsx new file mode 100644 index 00000000000..f1ddab82de3 --- /dev/null +++ b/public/app/features/correlations/CorrelationsPageWrapper.test.tsx @@ -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(); + expect(mockUseCorrelationsK8s).toHaveBeenCalled(); + }); + }); + + describe('with the kubernetes feature toggle off', () => { + it('uses the legacy correlations hook', () => { + config.featureToggles = { ...originalFeatureToggles, kubernetesCorrelations: false }; + render(); + expect(mockUseCorrelations).toHaveBeenCalled(); + }); + }); +}); diff --git a/public/app/features/correlations/CorrelationsPageWrapper.tsx b/public/app/features/correlations/CorrelationsPageWrapper.tsx new file mode 100644 index 00000000000..f511252be60 --- /dev/null +++ b/public/app/features/correlations/CorrelationsPageWrapper.tsx @@ -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 ( + + ); +} + +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 ( + { + 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 ; + } + + return ; +} diff --git a/public/app/features/correlations/useCorrelations.ts b/public/app/features/correlations/useCorrelations.ts index 1ae71f1bc76..5891e510573 100644 --- a/public/app/features/correlations/useCorrelations.ts +++ b/public/app/features/correlations/useCorrelations.ts @@ -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>( - (params) => - lastValueFrom( + async (params) => { + return lastValueFrom( backend.fetch({ 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>( - ({ sourceUID, ...correlation }) => - backend + async ({ sourceUID, ...correlation }) => { + return backend .post(`/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] ); diff --git a/public/app/features/correlations/useCorrelationsK8s.ts b/public/app/features/correlations/useCorrelationsK8s.ts new file mode 100644 index 00000000000..452b54c3078 --- /dev/null +++ b/public/app/features/correlations/useCorrelationsK8s.ts @@ -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, + }; +}; diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 8b8b3213004..451c92b4848 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -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') ), }, {