mirror of
https://github.com/grafana/grafana.git
synced 2026-02-03 20:49:50 -05:00
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:
parent
6e216b2af2
commit
2d9ec64be0
16 changed files with 370 additions and 63 deletions
|
|
@ -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
|
||||
|
|
@ -31,7 +31,9 @@ ConfigSpec: {
|
|||
transformations?: [...TransformationSpec]
|
||||
}
|
||||
|
||||
TargetSpec: [string]: _
|
||||
TargetSpec: {
|
||||
...
|
||||
}
|
||||
|
||||
TransformationSpec: {
|
||||
type: "regex" | "logfmt"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
67
public/app/features/correlations/CorrelationsPageWrapper.tsx
Normal file
67
public/app/features/correlations/CorrelationsPageWrapper.tsx
Normal 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 />;
|
||||
}
|
||||
|
|
@ -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]
|
||||
);
|
||||
|
||||
|
|
|
|||
94
public/app/features/correlations/useCorrelationsK8s.ts
Normal file
94
public/app/features/correlations/useCorrelationsK8s.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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')
|
||||
),
|
||||
},
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue