Correlations: Create paginated API (#65241)

* Add pagination params and apply to sql

* Create getCorrelationsResponse that returns metadata

* Set up pagination, change correlations fetch to only get source datasource correlations

* Move correlations from root to pane, only fetch correlations for one datasource when initialized or datasource is changed

* Fix tests

* Fix remaining tests

* Use functional component to handle state

* Remove unneeded mocks, fix tests

* Change perPage to limit

* Fix Go Tests

* Fix linter

* Remove parameter

* Account for mixed datasources

* Delete unused hook

* add source UID filter to API, start backing out front end hook changes

* add source IDs to API, use when loading or changing datasource

* Fix prettier

* Mock correlations response

* Get correlations for all datasources in mixed scenario

* Add documentation for new parameters

* Attempt to fix swagger

* Fix correlations page

* add swagger and openapi docs

* Add mocks to failing test

* Change API for consistency, remove extra hooks and unused function

* Add max to limit and re-gen api docs

* Move the page to the previous page if deleting all the rows on the page

* Only fetch if remove does not have value

* Change page to a reference hook

* Fix documentation, a test and some logic thinking page could be 0
This commit is contained in:
Kristina 2023-07-05 09:37:17 -05:00 committed by GitHub
parent 340c536d0e
commit f18a02149a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 789 additions and 6895 deletions

View File

@ -267,6 +267,12 @@ Status codes:
Get all correlations. Get all correlations.
Query parameters:
- **page** - Optional. Specify which page number to return. Use the limit parameter to specify the number of correlations per page. The default is page 1.
- **limit** - Optional. Limits the number of returned correlations per page. The default is 100 correlations per page. The maximum limit is 1000 correlations in a page.
- **sourceUID** - Optional. Specify a source datasource UID to filter by. This can be repeated to filter by multiple datasources.
**Example request:** **Example request:**
```http ```http

View File

@ -102,7 +102,7 @@ export const Pagination = ({
<ol> <ol>
<li className={styles.item}> <li className={styles.item}>
<Button <Button
aria-label="previous" aria-label={`previous page`}
size="sm" size="sm"
variant="secondary" variant="secondary"
onClick={() => onNavigate(currentPage - 1)} onClick={() => onNavigate(currentPage - 1)}
@ -114,7 +114,7 @@ export const Pagination = ({
{pageButtons} {pageButtons}
<li className={styles.item}> <li className={styles.item}>
<Button <Button
aria-label="next" aria-label={`next page`}
size="sm" size="sm"
variant="secondary" variant="secondary"
onClick={() => onNavigate(currentPage + 1)} onClick={() => onNavigate(currentPage + 1)}

View File

@ -76,7 +76,7 @@ type CreateCorrelationParams struct {
SourceUID string `json:"sourceUID"` SourceUID string `json:"sourceUID"`
} }
//swagger:response createCorrelationResponse // swagger:response createCorrelationResponse
type CreateCorrelationResponse struct { type CreateCorrelationResponse struct {
// in: body // in: body
Body CreateCorrelationResponseBody `json:"body"` Body CreateCorrelationResponseBody `json:"body"`
@ -192,7 +192,7 @@ type UpdateCorrelationParams struct {
Body UpdateCorrelationCommand `json:"body"` Body UpdateCorrelationCommand `json:"body"`
} }
//swagger:response updateCorrelationResponse // swagger:response updateCorrelationResponse
type UpdateCorrelationResponse struct { type UpdateCorrelationResponse struct {
// in: body // in: body
Body UpdateCorrelationResponseBody `json:"body"` Body UpdateCorrelationResponseBody `json:"body"`
@ -282,7 +282,7 @@ type GetCorrelationsBySourceUIDParams struct {
DatasourceUID string `json:"sourceUID"` DatasourceUID string `json:"sourceUID"`
} }
//swagger:response getCorrelationsBySourceUIDResponse // swagger:response getCorrelationsBySourceUIDResponse
type GetCorrelationsBySourceUIDResponse struct { type GetCorrelationsBySourceUIDResponse struct {
// in: body // in: body
Body []Correlation `json:"body"` Body []Correlation `json:"body"`
@ -298,8 +298,25 @@ type GetCorrelationsBySourceUIDResponse struct {
// 404: notFoundError // 404: notFoundError
// 500: internalServerError // 500: internalServerError
func (s *CorrelationsService) getCorrelationsHandler(c *contextmodel.ReqContext) response.Response { func (s *CorrelationsService) getCorrelationsHandler(c *contextmodel.ReqContext) response.Response {
limit := c.QueryInt64("limit")
if limit <= 0 {
limit = 100
} else if limit > 1000 {
limit = 1000
}
page := c.QueryInt64("page")
if page <= 0 {
page = 1
}
sourceUIDs := c.QueryStrings("sourceUID")
query := GetCorrelationsQuery{ query := GetCorrelationsQuery{
OrgId: c.OrgID, OrgId: c.OrgID,
Limit: limit,
Page: page,
SourceUIDs: sourceUIDs,
} }
correlations, err := s.getCorrelations(c.Req.Context(), query) correlations, err := s.getCorrelations(c.Req.Context(), query)
@ -314,6 +331,27 @@ func (s *CorrelationsService) getCorrelationsHandler(c *contextmodel.ReqContext)
return response.JSON(http.StatusOK, correlations) return response.JSON(http.StatusOK, correlations)
} }
// swagger:parameters getCorrelations
type GetCorrelationsParams struct {
// Limit the maximum number of correlations to return per page
// in:query
// required:false
// default:100
// maximum: 1000
Limit int64 `json:"limit"`
// Page index for starting fetching correlations
// in:query
// required:false
// default:1
Page int64 `json:"page"`
// Source datasource UID filter to be applied to correlations
// in:query
// type: array
// collectionFormat: multi
// required:false
SourceUIDs []string `json:"sourceUID"`
}
//swagger:response getCorrelationsResponse //swagger:response getCorrelationsResponse
type GetCorrelationsResponse struct { type GetCorrelationsResponse struct {
// in: body // in: body

View File

@ -94,7 +94,7 @@ func (s CorrelationsService) GetCorrelationsBySourceUID(ctx context.Context, cmd
return s.getCorrelationsBySourceUID(ctx, cmd) return s.getCorrelationsBySourceUID(ctx, cmd)
} }
func (s CorrelationsService) GetCorrelations(ctx context.Context, cmd GetCorrelationsQuery) ([]Correlation, error) { func (s CorrelationsService) GetCorrelations(ctx context.Context, cmd GetCorrelationsQuery) (GetCorrelationsResponseBody, error) {
return s.getCorrelations(ctx, cmd) return s.getCorrelations(ctx, cmd)
} }

View File

@ -225,17 +225,42 @@ func (s CorrelationsService) getCorrelationsBySourceUID(ctx context.Context, cmd
return correlations, nil return correlations, nil
} }
func (s CorrelationsService) getCorrelations(ctx context.Context, cmd GetCorrelationsQuery) ([]Correlation, error) { func (s CorrelationsService) getCorrelations(ctx context.Context, cmd GetCorrelationsQuery) (GetCorrelationsResponseBody, error) {
correlations := make([]Correlation, 0) result := GetCorrelationsResponseBody{
Correlations: make([]Correlation, 0),
err := s.SQLStore.WithDbSession(ctx, func(session *db.Session) error { Page: cmd.Page,
return session.Select("correlation.*").Join("", "data_source AS dss", "correlation.source_uid = dss.uid and dss.org_id = ?", cmd.OrgId).Join("", "data_source AS dst", "correlation.target_uid = dst.uid and dst.org_id = ?", cmd.OrgId).Find(&correlations) Limit: cmd.Limit,
})
if err != nil {
return []Correlation{}, err
} }
return correlations, nil err := s.SQLStore.WithDbSession(ctx, func(session *db.Session) error {
offset := cmd.Limit * (cmd.Page - 1)
q := session.Select("correlation.*").Join("", "data_source AS dss", "correlation.source_uid = dss.uid and dss.org_id = ?", cmd.OrgId).Join("", "data_source AS dst", "correlation.target_uid = dst.uid and dst.org_id = ?", cmd.OrgId)
if len(cmd.SourceUIDs) > 0 {
q.In("dss.uid", cmd.SourceUIDs)
}
return q.Limit(int(cmd.Limit), int(offset)).Find(&result.Correlations)
})
if err != nil {
return GetCorrelationsResponseBody{}, err
}
count, err := s.CountCorrelations(ctx)
if err != nil {
return GetCorrelationsResponseBody{}, err
}
tag, err := quota.NewTag(QuotaTargetSrv, QuotaTarget, quota.GlobalScope)
if err != nil {
return GetCorrelationsResponseBody{}, err
}
totalCount, _ := count.Get(tag)
result.TotalCount = totalCount
return result, nil
} }
func (s CorrelationsService) deleteCorrelationsBySourceUID(ctx context.Context, cmd DeleteCorrelationsBySourceUIDCommand) error { func (s CorrelationsService) deleteCorrelationsBySourceUID(ctx context.Context, cmd DeleteCorrelationsBySourceUIDCommand) error {

View File

@ -73,11 +73,11 @@ type CorrelationConfig struct {
Type CorrelationConfigType `json:"type" binding:"Required"` Type CorrelationConfigType `json:"type" binding:"Required"`
// Target data query // Target data query
// required:true // required:true
// example: { "expr": "job=app" } // example: {"prop1":"value1","prop2":"value"}
Target map[string]interface{} `json:"target" binding:"Required"` Target map[string]interface{} `json:"target" binding:"Required"`
// Source data transformations // Source data transformations
// required:false // required:false
// example: [{"type": "logfmt"}] // example: [{"type":"logfmt"}]
Transformations Transformations `json:"transformations,omitempty"` Transformations Transformations `json:"transformations,omitempty"`
} }
@ -107,10 +107,10 @@ type Correlation struct {
// example: 50xhMlg9k // example: 50xhMlg9k
UID string `json:"uid" xorm:"pk 'uid'"` UID string `json:"uid" xorm:"pk 'uid'"`
// UID of the data source the correlation originates from // UID of the data source the correlation originates from
// example:d0oxYRg4z // example: d0oxYRg4z
SourceUID string `json:"sourceUID" xorm:"pk 'source_uid'"` SourceUID string `json:"sourceUID" xorm:"pk 'source_uid'"`
// UID of the data source the correlation points to // UID of the data source the correlation points to
// example:PE1C5CBDA0504A6A3 // example: PE1C5CBDA0504A6A3
TargetUID *string `json:"targetUID" xorm:"target_uid"` TargetUID *string `json:"targetUID" xorm:"target_uid"`
// Label identifying the correlation // Label identifying the correlation
// example: My Label // example: My Label
@ -122,6 +122,13 @@ type Correlation struct {
Config CorrelationConfig `json:"config" xorm:"jsonb config"` Config CorrelationConfig `json:"config" xorm:"jsonb config"`
} }
type GetCorrelationsResponseBody struct {
Correlations []Correlation `json:"correlations"`
TotalCount int64 `json:"totalCount"`
Page int64 `json:"page"`
Limit int64 `json:"limit"`
}
// CreateCorrelationResponse is the response struct for CreateCorrelationCommand // CreateCorrelationResponse is the response struct for CreateCorrelationCommand
// swagger:model // swagger:model
type CreateCorrelationResponseBody struct { type CreateCorrelationResponseBody struct {
@ -138,7 +145,7 @@ type CreateCorrelationCommand struct {
OrgId int64 `json:"-"` OrgId int64 `json:"-"`
SkipReadOnlyCheck bool `json:"-"` SkipReadOnlyCheck bool `json:"-"`
// Target data source UID to which the correlation is created. required if config.type = query // Target data source UID to which the correlation is created. required if config.type = query
// example:PE1C5CBDA0504A6A3 // example: PE1C5CBDA0504A6A3
TargetUID *string `json:"targetUID"` TargetUID *string `json:"targetUID"`
// Optional label identifying the correlation // Optional label identifying the correlation
// example: My label // example: My label
@ -193,7 +200,7 @@ type CorrelationConfigUpdateDTO struct {
// Target type // Target type
Type *CorrelationConfigType `json:"type"` Type *CorrelationConfigType `json:"type"`
// Target data query // Target data query
// example: { "expr": "job=app" } // example: {"prop1":"value1","prop2":"value"}
Target *map[string]interface{} `json:"target"` Target *map[string]interface{} `json:"target"`
// Source data transformations // Source data transformations
// example: [{"type": "logfmt"},{"type":"regex","expression":"(Superman|Batman)", "variable":"name"}] // example: [{"type": "logfmt"},{"type":"regex","expression":"(Superman|Batman)", "variable":"name"}]
@ -260,6 +267,21 @@ type GetCorrelationsBySourceUIDQuery struct {
// GetCorrelationsQuery is the query to retrieve all correlations // GetCorrelationsQuery is the query to retrieve all correlations
type GetCorrelationsQuery struct { type GetCorrelationsQuery struct {
OrgId int64 `json:"-"` OrgId int64 `json:"-"`
// Limit the maximum number of correlations to return per page
// in:query
// required:false
// default:100
Limit int64 `json:"limit"`
// Page index for starting fetching correlations
// in:query
// required:false
// default:1
Page int64 `json:"page"`
// Source datasource UID filter to be applied to correlations
// in:query
// required:false
SourceUIDs []string `json:"sourceuid"`
} }
type DeleteCorrelationsBySourceUIDCommand struct { type DeleteCorrelationsBySourceUIDCommand struct {

View File

@ -51,12 +51,13 @@ type User struct {
type GetParams struct { type GetParams struct {
url string url string
user User user User
page string
} }
func (c TestContext) Get(params GetParams) *http.Response { func (c TestContext) Get(params GetParams) *http.Response {
c.t.Helper() c.t.Helper()
fmtUrl := fmt.Sprintf("%s?page=%s", params.url, params.page)
resp, err := http.Get(c.getURL(params.url, params.user)) resp, err := http.Get(c.getURL(fmtUrl, params.user))
require.NoError(c.t, err) require.NoError(c.t, err)
return resp return resp

View File

@ -42,17 +42,18 @@ func TestIntegrationReadCorrelation(t *testing.T) {
res := ctx.Get(GetParams{ res := ctx.Get(GetParams{
url: "/api/datasources/correlations", url: "/api/datasources/correlations",
user: adminUser, user: adminUser,
page: "0",
}) })
require.Equal(t, http.StatusOK, res.StatusCode) require.Equal(t, http.StatusOK, res.StatusCode)
responseBody, err := io.ReadAll(res.Body) responseBody, err := io.ReadAll(res.Body)
require.NoError(t, err) require.NoError(t, err)
var response []correlations.Correlation var response correlations.GetCorrelationsResponseBody
err = json.Unmarshal(responseBody, &response) err = json.Unmarshal(responseBody, &response)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, response, 0) require.Len(t, response.Correlations, 0)
require.NoError(t, res.Body.Close()) require.NoError(t, res.Body.Close())
}) })
@ -147,12 +148,12 @@ func TestIntegrationReadCorrelation(t *testing.T) {
responseBody, err := io.ReadAll(res.Body) responseBody, err := io.ReadAll(res.Body)
require.NoError(t, err) require.NoError(t, err)
var response []correlations.Correlation var response correlations.GetCorrelationsResponseBody
err = json.Unmarshal(responseBody, &response) err = json.Unmarshal(responseBody, &response)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, response, 1) require.Len(t, response.Correlations, 1)
require.EqualValues(t, correlation, response[0]) require.EqualValues(t, correlation, response.Correlations[0])
require.NoError(t, res.Body.Close()) require.NoError(t, res.Body.Close())
}) })

File diff suppressed because it is too large Load Diff

View File

@ -73,7 +73,14 @@ const renderWithContext = async (
}, },
fetch: (options: BackendSrvRequest) => { fetch: (options: BackendSrvRequest) => {
return new Observable((s) => { return new Observable((s) => {
s.next(merge(createFetchCorrelationsResponse({ url: options.url, data: correlations }))); s.next(
merge(
createFetchCorrelationsResponse({
url: options.url,
data: { correlations, page: 1, limit: 5, totalCount: 0 },
})
)
);
s.complete(); s.complete();
}); });
}, },

View File

@ -1,6 +1,6 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { negate } from 'lodash'; import { negate } from 'lodash';
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { isFetchError, reportInteraction } from '@grafana/runtime'; import { isFetchError, reportInteraction } from '@grafana/runtime';
@ -15,6 +15,7 @@ import {
type Column, type Column,
type CellProps, type CellProps,
type SortByFn, type SortByFn,
Pagination,
Icon, Icon,
} from '@grafana/ui'; } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page'; import { Page } from 'app/core/components/Page/Page';
@ -41,6 +42,7 @@ const loaderWrapper = css`
export default function CorrelationsPage() { export default function CorrelationsPage() {
const navModel = useNavModel('correlations'); const navModel = useNavModel('correlations');
const [isAdding, setIsAddingValue] = useState(false); const [isAdding, setIsAddingValue] = useState(false);
const page = useRef(1);
const setIsAdding = (value: boolean) => { const setIsAdding = (value: boolean) => {
setIsAddingValue(value); setIsAddingValue(value);
@ -54,61 +56,59 @@ export default function CorrelationsPage() {
get: { execute: fetchCorrelations, ...get }, get: { execute: fetchCorrelations, ...get },
} = useCorrelations(); } = useCorrelations();
useEffect(() => {
fetchCorrelations();
// we only want to fetch data on first render
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const canWriteCorrelations = contextSrv.hasPermission(AccessControlAction.DataSourcesWrite); const canWriteCorrelations = contextSrv.hasPermission(AccessControlAction.DataSourcesWrite);
const handleAdded = useCallback(() => { const handleAdded = useCallback(() => {
reportInteraction('grafana_correlations_added'); reportInteraction('grafana_correlations_added');
fetchCorrelations(); fetchCorrelations({ page: page.current });
setIsAdding(false); setIsAdding(false);
}, [fetchCorrelations]); }, [fetchCorrelations]);
const handleUpdated = useCallback(() => { const handleUpdated = useCallback(() => {
reportInteraction('grafana_correlations_edited'); reportInteraction('grafana_correlations_edited');
fetchCorrelations(); fetchCorrelations({ page: page.current });
}, [fetchCorrelations]); }, [fetchCorrelations]);
const handleDelete = useCallback( const handleDelete = useCallback(
(params: RemoveCorrelationParams) => { async (params: RemoveCorrelationParams, isLastRow: boolean) => {
remove.execute(params); await remove.execute(params);
reportInteraction('grafana_correlations_deleted');
if (isLastRow) {
page.current--;
}
fetchCorrelations({ page: page.current });
}, },
[remove] [remove, fetchCorrelations]
); );
// onDelete - triggers when deleting a correlation
useEffect(() => { useEffect(() => {
if (remove.value) { fetchCorrelations({ page: page.current });
reportInteraction('grafana_correlations_deleted'); }, [fetchCorrelations]);
}
}, [remove.value]);
useEffect(() => {
if (!remove.error && !remove.loading && remove.value) {
fetchCorrelations();
}
}, [remove.error, remove.loading, remove.value, fetchCorrelations]);
const RowActions = useCallback( const RowActions = useCallback(
({ ({
row: { row: {
index,
original: { original: {
source: { uid: sourceUID, readOnly }, source: { uid: sourceUID, readOnly },
uid, uid,
}, },
}, },
}: CellProps<CorrelationData, void>) => }: CellProps<CorrelationData, void>) => {
!readOnly && ( return (
<DeleteButton !readOnly && (
aria-label="delete correlation" <DeleteButton
onConfirm={() => handleDelete({ sourceUID, uid })} aria-label="delete correlation"
closeOnConfirm onConfirm={() =>
/> handleDelete({ sourceUID, uid }, page.current > 1 && index === 0 && data?.correlations.length === 1)
), }
closeOnConfirm
/>
)
);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[handleDelete] [handleDelete]
); );
@ -144,8 +144,8 @@ export default function CorrelationsPage() {
); );
const data = useMemo(() => get.value, [get.value]); const data = useMemo(() => get.value, [get.value]);
const showEmptyListCTA = data?.length === 0 && !isAdding && !get.error; const showEmptyListCTA = data?.correlations.length === 0 && !isAdding && !get.error;
const addButton = canWriteCorrelations && data?.length !== 0 && data !== undefined && !isAdding && ( const addButton = canWriteCorrelations && data?.correlations?.length !== 0 && data !== undefined && !isAdding && (
<Button icon="plus" onClick={() => setIsAdding(true)}> <Button icon="plus" onClick={() => setIsAdding(true)}>
Add new Add new
</Button> </Button>
@ -188,19 +188,28 @@ export default function CorrelationsPage() {
{isAdding && <AddCorrelationForm onClose={() => setIsAdding(false)} onCreated={handleAdded} />} {isAdding && <AddCorrelationForm onClose={() => setIsAdding(false)} onCreated={handleAdded} />}
{data && data.length >= 1 && ( {data && data.correlations.length >= 1 && (
<InteractiveTable <>
renderExpandedRow={(correlation) => ( <InteractiveTable
<ExpendedRow renderExpandedRow={(correlation) => (
correlation={correlation} <ExpendedRow
onUpdated={handleUpdated} correlation={correlation}
readOnly={isSourceReadOnly({ source: correlation.source }) || !canWriteCorrelations} onUpdated={handleUpdated}
/> readOnly={isSourceReadOnly({ source: correlation.source }) || !canWriteCorrelations}
)} />
columns={columns} )}
data={data} columns={columns}
getRowId={(correlation) => `${correlation.source.uid}-${correlation.uid}`} data={data.correlations}
/> getRowId={(correlation) => `${correlation.source.uid}-${correlation.uid}`}
/>
<Pagination
currentPage={page.current}
numberOfPages={Math.ceil(data.totalCount / data.limit)}
onNavigate={(toPage: number) => {
fetchCorrelations({ page: (page.current = toPage) });
}}
/>
</>
)} )}
</div> </div>
</Page.Contents> </Page.Contents>

View File

@ -44,6 +44,10 @@ export interface Correlation {
config: CorrelationConfig; config: CorrelationConfig;
} }
export type GetCorrelationsParams = {
page: number;
};
export type RemoveCorrelationParams = Pick<Correlation, 'sourceUID' | 'uid'>; export type RemoveCorrelationParams = Pick<Correlation, 'sourceUID' | 'uid'>;
export type CreateCorrelationParams = Omit<Correlation, 'uid'>; export type CreateCorrelationParams = Omit<Correlation, 'uid'>;
export type UpdateCorrelationParams = Omit<Correlation, 'targetUID'>; export type UpdateCorrelationParams = Omit<Correlation, 'targetUID'>;

View File

@ -9,17 +9,32 @@ import {
Correlation, Correlation,
CreateCorrelationParams, CreateCorrelationParams,
CreateCorrelationResponse, CreateCorrelationResponse,
GetCorrelationsParams,
RemoveCorrelationParams, RemoveCorrelationParams,
RemoveCorrelationResponse, RemoveCorrelationResponse,
UpdateCorrelationParams, UpdateCorrelationParams,
UpdateCorrelationResponse, UpdateCorrelationResponse,
} from './types'; } from './types';
export interface CorrelationsResponse {
correlations: Correlation[];
page: number;
limit: number;
totalCount: number;
}
export interface CorrelationData extends Omit<Correlation, 'sourceUID' | 'targetUID'> { export interface CorrelationData extends Omit<Correlation, 'sourceUID' | 'targetUID'> {
source: DataSourceInstanceSettings; source: DataSourceInstanceSettings;
target: DataSourceInstanceSettings; target: DataSourceInstanceSettings;
} }
export interface CorrelationsData {
correlations: CorrelationData[];
page: number;
limit: number;
totalCount: number;
}
const toEnrichedCorrelationData = ({ const toEnrichedCorrelationData = ({
sourceUID, sourceUID,
targetUID, targetUID,
@ -39,10 +54,14 @@ const toEnrichedCorrelationData = ({
const validSourceFilter = (correlation: CorrelationData | undefined): correlation is CorrelationData => !!correlation; const validSourceFilter = (correlation: CorrelationData | undefined): correlation is CorrelationData => !!correlation;
const toEnrichedCorrelationsData = (correlations: Correlation[]): CorrelationData[] => { export const toEnrichedCorrelationsData = (correlationsResponse: CorrelationsResponse): CorrelationsData => {
return correlations.map(toEnrichedCorrelationData).filter(validSourceFilter); return {
...correlationsResponse,
correlations: correlationsResponse.correlations.map(toEnrichedCorrelationData).filter(validSourceFilter),
};
}; };
function getData<T>(response: FetchResponse<T>) {
export function getData<T>(response: FetchResponse<T>) {
return response.data; return response.data;
} }
@ -55,10 +74,15 @@ function getData<T>(response: FetchResponse<T>) {
export const useCorrelations = () => { export const useCorrelations = () => {
const { backend } = useGrafana(); const { backend } = useGrafana();
const [getInfo, get] = useAsyncFn<() => Promise<CorrelationData[]>>( const [getInfo, get] = useAsyncFn<(params: GetCorrelationsParams) => Promise<CorrelationsData>>(
() => (params) =>
lastValueFrom( lastValueFrom(
backend.fetch<Correlation[]>({ url: '/api/datasources/correlations', method: 'GET', showErrorAlert: false }) backend.fetch<CorrelationsResponse>({
url: '/api/datasources/correlations',
params: { page: params.page },
method: 'GET',
showErrorAlert: false,
})
) )
.then(getData) .then(getData)
.then(toEnrichedCorrelationsData), .then(toEnrichedCorrelationsData),

View File

@ -1,8 +1,17 @@
import { lastValueFrom } from 'rxjs';
import { DataFrame, DataLinkConfigOrigin } from '@grafana/data'; import { DataFrame, DataLinkConfigOrigin } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { formatValueName } from '../explore/PrometheusListView/ItemLabels'; import { formatValueName } from '../explore/PrometheusListView/ItemLabels';
import { CorrelationData } from './useCorrelations'; import {
CorrelationData,
CorrelationsData,
CorrelationsResponse,
getData,
toEnrichedCorrelationsData,
} from './useCorrelations';
type DataFrameRefIdToDataSourceUid = Record<string, string>; type DataFrameRefIdToDataSourceUid = Record<string, string>;
@ -58,3 +67,18 @@ const decorateDataFrameWithInternalDataLinks = (dataFrame: DataFrame, correlatio
}); });
}); });
}; };
export const getCorrelationsBySourceUIDs = async (sourceUIDs: string[]): Promise<CorrelationsData> => {
return lastValueFrom(
getBackendSrv().fetch<CorrelationsResponse>({
url: `/api/datasources/correlations`,
method: 'GET',
showErrorAlert: false,
params: {
sourceUID: sourceUIDs,
},
})
)
.then(getData)
.then(toEnrichedCorrelationsData);
};

View File

@ -31,6 +31,20 @@ jest.mock('react-virtualized-auto-sizer', () => {
}; };
}); });
const fetch = jest.fn().mockResolvedValue({ correlations: [] });
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => ({ fetch }),
}));
jest.mock('rxjs', () => ({
...jest.requireActual('rxjs'),
lastValueFrom: () =>
new Promise((resolve, reject) => {
resolve({ data: { correlations: [] } });
}),
}));
describe('ExplorePage', () => { describe('ExplorePage', () => {
afterEach(() => { afterEach(() => {
tearDown(); tearDown();

View File

@ -13,7 +13,6 @@ import { ExploreQueryParams } from 'app/types/explore';
import { ExploreActions } from './ExploreActions'; import { ExploreActions } from './ExploreActions';
import { ExplorePaneContainer } from './ExplorePaneContainer'; import { ExplorePaneContainer } from './ExplorePaneContainer';
import { useExploreCorrelations } from './hooks/useExploreCorrelations';
import { useExplorePageTitle } from './hooks/useExplorePageTitle'; import { useExplorePageTitle } from './hooks/useExplorePageTitle';
import { useStateSync } from './hooks/useStateSync'; import { useStateSync } from './hooks/useStateSync';
import { useTimeSrvFix } from './hooks/useTimeSrvFix'; import { useTimeSrvFix } from './hooks/useTimeSrvFix';
@ -39,7 +38,6 @@ export default function ExplorePage(props: GrafanaRouteComponentProps<{}, Explor
// if we were to update the URL on state change, the title would not match the URL. // if we were to update the URL on state change, the title would not match the URL.
// Ultimately the URL is the single source of truth from which state is derived, the page title is not different // Ultimately the URL is the single source of truth from which state is derived, the page title is not different
useExplorePageTitle(props.queryParams); useExplorePageTitle(props.queryParams);
useExploreCorrelations();
const dispatch = useDispatch(); const dispatch = useDispatch();
const { keybindings, chrome } = useGrafana(); const { keybindings, chrome } = useGrafana();
const navModel = useNavModel('explore'); const navModel = useNavModel('explore');

View File

@ -56,10 +56,10 @@ function setup(queries: DataQuery[]) {
richHistory: [], richHistory: [],
datasourceInstance: datasources['someDs-uid'], datasourceInstance: datasources['someDs-uid'],
queries, queries,
correlations: [],
}, },
}, },
syncedTimes: false, syncedTimes: false,
correlations: [],
richHistoryStorageFull: false, richHistoryStorageFull: false,
richHistoryLimitExceededWarningShown: false, richHistoryLimitExceededWarningShown: false,
}; };

View File

@ -1,35 +0,0 @@
import { useEffect } from 'react';
import { config } from '@grafana/runtime';
import { useAppNotification } from 'app/core/copy/appNotification';
import { useCorrelations } from 'app/features/correlations/useCorrelations';
import { useDispatch } from 'app/types';
import { saveCorrelationsAction } from '../state/main';
export function useExploreCorrelations() {
const { get } = useCorrelations();
const { warning } = useAppNotification();
const dispatch = useDispatch();
useEffect(() => {
if (!config.featureToggles.correlations) {
dispatch(saveCorrelationsAction([]));
} else {
get.execute();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (get.value) {
dispatch(saveCorrelationsAction(get.value));
} else if (get.error) {
dispatch(saveCorrelationsAction([]));
warning(
'Could not load correlations.',
'Correlations data could not be loaded, DataLinks may have partial data.'
);
}
}, [get.value, get.error, dispatch, warning]);
}

View File

@ -17,6 +17,20 @@ import { splitClose, splitOpen } from '../../state/main';
import { useStateSync } from './'; import { useStateSync } from './';
const fetch = jest.fn().mockResolvedValue({ correlations: [] });
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => ({ fetch }),
}));
jest.mock('rxjs', () => ({
...jest.requireActual('rxjs'),
lastValueFrom: () =>
new Promise((resolve, reject) => {
resolve({ data: { correlations: [] } });
}),
}));
function defaultDsGetter(datasources: Array<ReturnType<typeof makeDatasourceSetup>>): DataSourceSrv['get'] { function defaultDsGetter(datasources: Array<ReturnType<typeof makeDatasourceSetup>>): DataSourceSrv['get'] {
return (datasource) => { return (datasource) => {
let ds; let ds;

View File

@ -4,6 +4,12 @@ import { changeDatasource } from './helper/interactions';
import { makeLogsQueryResponse } from './helper/query'; import { makeLogsQueryResponse } from './helper/query';
import { setupExplore, tearDown, waitForExplore } from './helper/setup'; import { setupExplore, tearDown, waitForExplore } from './helper/setup';
jest.mock('../../correlations/utils', () => {
return {
getCorrelationsBySourceUIDs: jest.fn().mockReturnValue({ correlations: [] }),
};
});
describe('Explore: handle datasource states', () => { describe('Explore: handle datasource states', () => {
afterEach(() => { afterEach(() => {
tearDown(); tearDown();

View File

@ -29,6 +29,12 @@ jest.mock('react-virtualized-auto-sizer', () => {
}; };
}); });
jest.mock('../../correlations/utils', () => {
return {
getCorrelationsBySourceUIDs: jest.fn().mockReturnValue({ correlations: [] }),
};
});
describe('Explore: interpolation', () => { describe('Explore: interpolation', () => {
// support-escalations/issues/1459 // support-escalations/issues/1459
it('Time is interpolated when explore is opened with a URL', async () => { it('Time is interpolated when explore is opened with a URL', async () => {

View File

@ -5,6 +5,12 @@ import { serializeStateToUrlParam } from '@grafana/data';
import { makeLogsQueryResponse } from './helper/query'; import { makeLogsQueryResponse } from './helper/query';
import { setupExplore, tearDown, waitForExplore } from './helper/setup'; import { setupExplore, tearDown, waitForExplore } from './helper/setup';
jest.mock('../../correlations/utils', () => {
return {
getCorrelationsBySourceUIDs: jest.fn().mockReturnValue({ correlations: [] }),
};
});
describe('Explore: handle running/not running query', () => { describe('Explore: handle running/not running query', () => {
afterEach(() => { afterEach(() => {
tearDown(); tearDown();

View File

@ -74,6 +74,12 @@ jest.mock('react-virtualized-auto-sizer', () => {
}; };
}); });
jest.mock('../../correlations/utils', () => {
return {
getCorrelationsBySourceUIDs: jest.fn().mockReturnValue({ correlations: [] }),
};
});
describe('Explore: Query History', () => { describe('Explore: Query History', () => {
const USER_INPUT = 'my query'; const USER_INPUT = 'my query';
const RAW_QUERY = `{"expr":"${USER_INPUT}"}`; const RAW_QUERY = `{"expr":"${USER_INPUT}"}`;

View File

@ -6,13 +6,15 @@ import { reportInteraction } from '@grafana/runtime';
import { DataSourceRef } from '@grafana/schema'; import { DataSourceRef } from '@grafana/schema';
import { RefreshPicker } from '@grafana/ui'; import { RefreshPicker } from '@grafana/ui';
import { stopQueryState } from 'app/core/utils/explore'; import { stopQueryState } from 'app/core/utils/explore';
import { getCorrelationsBySourceUIDs } from 'app/features/correlations/utils';
import { ExploreItemState, ThunkResult } from 'app/types'; import { ExploreItemState, ThunkResult } from 'app/types';
import { loadSupplementaryQueries } from '../utils/supplementaryQueries'; import { loadSupplementaryQueries } from '../utils/supplementaryQueries';
import { saveCorrelationsAction } from './explorePane';
import { importQueries, runQueries } from './query'; import { importQueries, runQueries } from './query';
import { changeRefreshInterval } from './time'; import { changeRefreshInterval } from './time';
import { createEmptyQueryResponse, loadAndInitDatasource } from './utils'; import { createEmptyQueryResponse, getDatasourceUIDs, loadAndInitDatasource } from './utils';
// //
// Actions and Payloads // Actions and Payloads
@ -60,8 +62,13 @@ export function changeDatasource(
}) })
); );
const queries = getState().explore.panes[exploreId]!.queries;
const datasourceUIDs = getDatasourceUIDs(instance.uid, queries);
const correlations = await getCorrelationsBySourceUIDs(datasourceUIDs);
dispatch(saveCorrelationsAction({ exploreId: exploreId, correlations: correlations.correlations || [] }));
if (options?.importQueries) { if (options?.importQueries) {
const queries = getState().explore.panes[exploreId]!.queries;
await dispatch(importQueries(exploreId, queries, currentDataSourceInstance, instance)); await dispatch(importQueries(exploreId, queries, currentDataSourceInstance, instance));
} }

View File

@ -11,6 +11,8 @@ import {
} from '@grafana/data'; } from '@grafana/data';
import { DataQuery, DataSourceRef } from '@grafana/schema'; import { DataQuery, DataSourceRef } from '@grafana/schema';
import { getQueryKeys } from 'app/core/utils/explore'; import { getQueryKeys } from 'app/core/utils/explore';
import { CorrelationData } from 'app/features/correlations/useCorrelations';
import { getCorrelationsBySourceUIDs } from 'app/features/correlations/utils';
import { getTimeZone } from 'app/features/profile/state/selectors'; import { getTimeZone } from 'app/features/profile/state/selectors';
import { createAsyncThunk, ThunkResult } from 'app/types'; import { createAsyncThunk, ThunkResult } from 'app/types';
import { ExploreItemState } from 'app/types/explore'; import { ExploreItemState } from 'app/types/explore';
@ -20,7 +22,13 @@ import { historyReducer } from './history';
import { richHistorySearchFiltersUpdatedAction, richHistoryUpdatedAction } from './main'; import { richHistorySearchFiltersUpdatedAction, richHistoryUpdatedAction } from './main';
import { queryReducer, runQueries } from './query'; import { queryReducer, runQueries } from './query';
import { timeReducer, updateTime } from './time'; import { timeReducer, updateTime } from './time';
import { makeExplorePaneState, loadAndInitDatasource, createEmptyQueryResponse, getRange } from './utils'; import {
makeExplorePaneState,
loadAndInitDatasource,
createEmptyQueryResponse,
getRange,
getDatasourceUIDs,
} from './utils';
// Types // Types
// //
@ -86,6 +94,12 @@ export interface SetUrlReplacedPayload {
} }
export const setUrlReplacedAction = createAction<SetUrlReplacedPayload>('explore/setUrlReplaced'); export const setUrlReplacedAction = createAction<SetUrlReplacedPayload>('explore/setUrlReplaced');
export interface SaveCorrelationsPayload {
exploreId: string;
correlations: CorrelationData[];
}
export const saveCorrelationsAction = createAction<SaveCorrelationsPayload>('explore/saveCorrelationsAction');
/** /**
* Keep track of the Explore container size, in particular the width. * Keep track of the Explore container size, in particular the width.
* The width will be used to calculate graph intervals (number of datapoints). * The width will be used to calculate graph intervals (number of datapoints).
@ -141,6 +155,10 @@ export const initializeExplore = createAsyncThunk(
dispatch(updateTime({ exploreId })); dispatch(updateTime({ exploreId }));
if (instance) { if (instance) {
const datasourceUIDs = getDatasourceUIDs(instance.uid, queries);
const correlations = await getCorrelationsBySourceUIDs(datasourceUIDs);
dispatch(saveCorrelationsAction({ exploreId: exploreId, correlations: correlations.correlations || [] }));
dispatch(runQueries({ exploreId })); dispatch(runQueries({ exploreId }));
} }
@ -189,6 +207,13 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
return { ...state, panelsState }; return { ...state, panelsState };
} }
if (saveCorrelationsAction.match(action)) {
return {
...state,
correlations: action.payload.correlations,
};
}
if (initializeExploreAction.match(action)) { if (initializeExploreAction.match(action)) {
const { queries, range, datasourceInstance, history } = action.payload; const { queries, range, datasourceInstance, history } = action.payload;
@ -202,6 +227,7 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
history, history,
queryResponse: createEmptyQueryResponse(), queryResponse: createEmptyQueryResponse(),
cache: [], cache: [],
correlations: [],
}; };
} }

View File

@ -10,7 +10,6 @@ import { ExploreItemState, ExploreState } from 'app/types/explore';
import { RichHistoryResults } from '../../../core/history/RichHistoryStorage'; import { RichHistoryResults } from '../../../core/history/RichHistoryStorage';
import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes'; import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes';
import { createAsyncThunk, ThunkResult } from '../../../types'; import { createAsyncThunk, ThunkResult } from '../../../types';
import { CorrelationData } from '../../correlations/useCorrelations';
import { TimeSrv } from '../../dashboard/services/TimeSrv'; import { TimeSrv } from '../../dashboard/services/TimeSrv';
import { withUniqueRefIds } from '../utils/queries'; import { withUniqueRefIds } from '../utils/queries';
@ -38,8 +37,6 @@ export const richHistorySearchFiltersUpdatedAction = createAction<{
filters?: RichHistorySearchFilters; filters?: RichHistorySearchFilters;
}>('explore/richHistorySearchFiltersUpdatedAction'); }>('explore/richHistorySearchFiltersUpdatedAction');
export const saveCorrelationsAction = createAction<CorrelationData[]>('explore/saveCorrelationsAction');
export const splitSizeUpdateAction = createAction<{ export const splitSizeUpdateAction = createAction<{
largerExploreId?: string; largerExploreId?: string;
}>('explore/splitSizeUpdateAction'); }>('explore/splitSizeUpdateAction');
@ -144,7 +141,6 @@ const initialExploreItemState = makeExplorePaneState();
export const initialExploreState: ExploreState = { export const initialExploreState: ExploreState = {
syncedTimes: false, syncedTimes: false,
panes: {}, panes: {},
correlations: undefined,
richHistoryStorageFull: false, richHistoryStorageFull: false,
richHistoryLimitExceededWarningShown: false, richHistoryLimitExceededWarningShown: false,
largerExploreId: undefined, largerExploreId: undefined,
@ -199,13 +195,6 @@ export const exploreReducer = (state = initialExploreState, action: AnyAction):
}; };
} }
if (saveCorrelationsAction.match(action)) {
return {
...state,
correlations: action.payload,
};
}
if (syncTimesAction.match(action)) { if (syncTimesAction.match(action)) {
return { ...state, syncedTimes: action.payload.syncedTimes }; return { ...state, syncedTimes: action.payload.syncedTimes };
} }

View File

@ -25,8 +25,8 @@ import { setTimeSrv, TimeSrv } from '../../dashboard/services/TimeSrv';
import { makeLogs } from '../__mocks__/makeLogs'; import { makeLogs } from '../__mocks__/makeLogs';
import { supplementaryQueryTypes } from '../utils/supplementaryQueries'; import { supplementaryQueryTypes } from '../utils/supplementaryQueries';
import { saveCorrelationsAction } from './explorePane';
import { createDefaultInitialState } from './helpers'; import { createDefaultInitialState } from './helpers';
import { saveCorrelationsAction } from './main';
import { import {
addQueryRowAction, addQueryRowAction,
addResultsToCache, addResultsToCache,
@ -159,7 +159,8 @@ describe('runQueries', () => {
it('should pass dataFrames to state even if there is error in response', async () => { it('should pass dataFrames to state even if there is error in response', async () => {
const { dispatch, getState } = setupTests(); const { dispatch, getState } = setupTests();
setupQueryResponse(getState()); setupQueryResponse(getState());
await dispatch(saveCorrelationsAction([]));
await dispatch(saveCorrelationsAction({ exploreId: 'left', correlations: [] }));
await dispatch(runQueries({ exploreId: 'left' })); await dispatch(runQueries({ exploreId: 'left' }));
expect(getState().explore.panes.left!.showMetrics).toBeTruthy(); expect(getState().explore.panes.left!.showMetrics).toBeTruthy();
expect(getState().explore.panes.left!.graphResult).toBeDefined(); expect(getState().explore.panes.left!.graphResult).toBeDefined();
@ -168,7 +169,7 @@ describe('runQueries', () => {
it('should modify the request-id for all supplementary queries', () => { it('should modify the request-id for all supplementary queries', () => {
const { dispatch, getState } = setupTests(); const { dispatch, getState } = setupTests();
setupQueryResponse(getState()); setupQueryResponse(getState());
dispatch(saveCorrelationsAction([])); dispatch(saveCorrelationsAction({ exploreId: 'left', correlations: [] }));
dispatch(runQueries({ exploreId: 'left' })); dispatch(runQueries({ exploreId: 'left' }));
const state = getState().explore.panes.left!; const state = getState().explore.panes.left!;
@ -188,7 +189,7 @@ describe('runQueries', () => {
const { dispatch, getState } = setupTests(); const { dispatch, getState } = setupTests();
const leftDatasourceInstance = assertIsDefined(getState().explore.panes.left!.datasourceInstance); const leftDatasourceInstance = assertIsDefined(getState().explore.panes.left!.datasourceInstance);
jest.mocked(leftDatasourceInstance.query).mockReturnValueOnce(EMPTY); jest.mocked(leftDatasourceInstance.query).mockReturnValueOnce(EMPTY);
await dispatch(saveCorrelationsAction([])); await dispatch(saveCorrelationsAction({ exploreId: 'left', correlations: [] }));
await dispatch(runQueries({ exploreId: 'left' })); await dispatch(runQueries({ exploreId: 'left' }));
await new Promise((resolve) => setTimeout(() => resolve(''), 500)); await new Promise((resolve) => setTimeout(() => resolve(''), 500));
expect(getState().explore.panes.left!.queryResponse.state).toBe(LoadingState.Done); expect(getState().explore.panes.left!.queryResponse.state).toBe(LoadingState.Done);
@ -199,7 +200,7 @@ describe('runQueries', () => {
setupQueryResponse(getState()); setupQueryResponse(getState());
await dispatch(runQueries({ exploreId: 'left' })); await dispatch(runQueries({ exploreId: 'left' }));
expect(getState().explore.panes.left!.graphResult).not.toBeDefined(); expect(getState().explore.panes.left!.graphResult).not.toBeDefined();
await dispatch(saveCorrelationsAction([])); await dispatch(saveCorrelationsAction({ exploreId: 'left', correlations: [] }));
expect(getState().explore.panes.left!.graphResult).toBeDefined(); expect(getState().explore.panes.left!.graphResult).toBeDefined();
}); });
}); });

View File

@ -35,6 +35,7 @@ import {
} from 'app/core/utils/explore'; } from 'app/core/utils/explore';
import { getShiftedTimeRange } from 'app/core/utils/timePicker'; import { getShiftedTimeRange } from 'app/core/utils/timePicker';
import { CorrelationData } from 'app/features/correlations/useCorrelations'; import { CorrelationData } from 'app/features/correlations/useCorrelations';
import { getCorrelationsBySourceUIDs } from 'app/features/correlations/utils';
import { getTimeZone } from 'app/features/profile/state/selectors'; import { getTimeZone } from 'app/features/profile/state/selectors';
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
import { store } from 'app/store/store'; import { store } from 'app/store/store';
@ -59,9 +60,10 @@ import {
supplementaryQueryTypes, supplementaryQueryTypes,
} from '../utils/supplementaryQueries'; } from '../utils/supplementaryQueries';
import { saveCorrelationsAction } from './explorePane';
import { addHistoryItem, historyUpdatedAction, loadRichHistory } from './history'; import { addHistoryItem, historyUpdatedAction, loadRichHistory } from './history';
import { updateTime } from './time'; import { updateTime } from './time';
import { createCacheKey, filterLogRowsByIndex, getResultsFromCache } from './utils'; import { createCacheKey, filterLogRowsByIndex, getDatasourceUIDs, getResultsFromCache } from './utils';
/** /**
* Derives from explore state if a given Explore pane is waiting for more data to be received * Derives from explore state if a given Explore pane is waiting for more data to be received
@ -319,6 +321,7 @@ export const changeQueries = createAsyncThunk<void, ChangeQueriesPayload>(
async ({ queries, exploreId }, { getState, dispatch }) => { async ({ queries, exploreId }, { getState, dispatch }) => {
let queriesImported = false; let queriesImported = false;
const oldQueries = getState().explore.panes[exploreId]!.queries; const oldQueries = getState().explore.panes[exploreId]!.queries;
const rootUID = getState().explore.panes[exploreId]!.datasourceInstance?.uid;
for (const newQuery of queries) { for (const newQuery of queries) {
for (const oldQuery of oldQueries) { for (const oldQuery of oldQueries) {
@ -328,6 +331,16 @@ export const changeQueries = createAsyncThunk<void, ChangeQueriesPayload>(
await dispatch(importQueries(exploreId, oldQueries, queryDatasource, targetDS, newQuery.refId)); await dispatch(importQueries(exploreId, oldQueries, queryDatasource, targetDS, newQuery.refId));
queriesImported = true; queriesImported = true;
} }
if (
rootUID === MIXED_DATASOURCE_NAME &&
newQuery.refId === oldQuery.refId &&
newQuery.datasource?.uid !== oldQuery.datasource?.uid
) {
const datasourceUIDs = getDatasourceUIDs(MIXED_DATASOURCE_NAME, queries);
const correlations = await getCorrelationsBySourceUIDs(datasourceUIDs);
dispatch(saveCorrelationsAction({ exploreId: exploreId, correlations: correlations.correlations || [] }));
}
} }
} }
@ -481,7 +494,7 @@ export const runQueries = createAsyncThunk<void, RunQueriesOptions>(
async ({ exploreId, preserveCache }, { dispatch, getState }) => { async ({ exploreId, preserveCache }, { dispatch, getState }) => {
dispatch(updateTime({ exploreId })); dispatch(updateTime({ exploreId }));
const correlations$ = getCorrelations(); const correlations$ = getCorrelations(exploreId);
// We always want to clear cache unless we explicitly pass preserveCache parameter // We always want to clear cache unless we explicitly pass preserveCache parameter
if (preserveCache !== true) { if (preserveCache !== true) {
@ -1134,15 +1147,15 @@ export const queryReducer = (state: ExploreItemState, action: AnyAction): Explor
/** /**
* Creates an observable that emits correlations once they are loaded * Creates an observable that emits correlations once they are loaded
*/ */
const getCorrelations = () => { const getCorrelations = (exploreId: string) => {
return new Observable<CorrelationData[]>((subscriber) => { return new Observable<CorrelationData[]>((subscriber) => {
const existingCorrelations = store.getState().explore.correlations; const existingCorrelations = store.getState().explore.panes[exploreId]?.correlations;
if (existingCorrelations) { if (existingCorrelations) {
subscriber.next(existingCorrelations); subscriber.next(existingCorrelations);
subscriber.complete(); subscriber.complete();
} else { } else {
const unsubscribe = store.subscribe(() => { const unsubscribe = store.subscribe(() => {
const { correlations } = store.getState().explore; const correlations = store.getState().explore.panes[exploreId]?.correlations;
if (correlations) { if (correlations) {
unsubscribe(); unsubscribe();
subscriber.next(correlations); subscriber.next(correlations);

View File

@ -1,3 +1,5 @@
import { uniq } from 'lodash';
import { import {
AbsoluteTimeRange, AbsoluteTimeRange,
DataSourceApi, DataSourceApi,
@ -15,7 +17,8 @@ import {
isDateTime, isDateTime,
toUtc, toUtc,
} from '@grafana/data'; } from '@grafana/data';
import { DataSourceRef, TimeZone } from '@grafana/schema'; import { DataQuery, DataSourceRef, TimeZone } from '@grafana/schema';
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
import { ExplorePanelData } from 'app/types'; import { ExplorePanelData } from 'app/types';
import { ExploreItemState } from 'app/types/explore'; import { ExploreItemState } from 'app/types/explore';
@ -67,6 +70,7 @@ export const makeExplorePaneState = (): ExploreItemState => ({
richHistory: [], richHistory: [],
supplementaryQueries: loadSupplementaryQueries(), supplementaryQueries: loadSupplementaryQueries(),
panelsState: {}, panelsState: {},
correlations: undefined,
}); });
export const createEmptyQueryResponse = (): ExplorePanelData => ({ export const createEmptyQueryResponse = (): ExplorePanelData => ({
@ -205,3 +209,11 @@ export const filterLogRowsByIndex = (
return logRows; return logRows;
}; };
export const getDatasourceUIDs = (datasourceUID: string, queries: DataQuery[]): string[] => {
if (datasourceUID === MIXED_DATASOURCE_NAME) {
return uniq(queries.map((query) => query.datasource?.uid).filter((uid): uid is string => !!uid));
} else {
return [datasourceUID];
}
};

View File

@ -33,8 +33,6 @@ export interface ExploreState {
panes: Record<string, ExploreItemState | undefined>; panes: Record<string, ExploreItemState | undefined>;
correlations?: CorrelationData[];
/** /**
* Settings for rich history (note: filters are stored per each pane separately) * Settings for rich history (note: filters are stored per each pane separately)
*/ */
@ -192,6 +190,8 @@ export interface ExploreItemState {
supplementaryQueries: SupplementaryQueries; supplementaryQueries: SupplementaryQueries;
panelsState: ExplorePanelsState; panelsState: ExplorePanelsState;
correlations?: CorrelationData[];
} }
export interface ExploreUpdateState { export interface ExploreUpdateState {

File diff suppressed because it is too large Load Diff