Query History: Remove migration (#67470)

This commit is contained in:
Giordano Ricci 2023-04-28 16:03:51 +01:00 committed by GitHub
parent 91471ac7ae
commit b5a2c3c7f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 3 additions and 677 deletions

View File

@ -335,62 +335,3 @@ Status codes:
- **200** OK - **200** OK
- **401** Unauthorized - **401** Unauthorized
- **500** Internal error - **500** Internal error
## Migrate queries to Query history
`POST /api/query-history/migrate`
Migrates multiple queries in to query history.
**Example request:**
```http
POST /api/query-history HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"queries": [
{
"datasourceUid": "PE1C5CBDA0504A6A3",
"queries": [
{
"refId": "A",
"key": "Q-87fed8e3-62ba-4eb2-8d2a-4129979bb4de-0",
"scenarioId": "csv_content",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
}
}
],
"starred": false,
"createdAt": 1643630762,
"comment": "debugging"
}
]
}
```
JSON body schema:
- **queries** JSON of query history items.
**Example response:**
```http
HTTP/1.1 200
Content-Type: application/json
{
"message": "Query history successfully migrated",
"totalCount": 105,
"starredCount": 10
}
```
Status codes:
- **200** OK
- **400** - Errors (invalid JSON, missing or invalid fields)
- **401** Unauthorized
- **500** Internal error

View File

@ -20,8 +20,6 @@ func (s *QueryHistoryService) registerAPIEndpoints() {
entities.Post("/star/:uid", middleware.ReqSignedIn, routing.Wrap(s.starHandler)) entities.Post("/star/:uid", middleware.ReqSignedIn, routing.Wrap(s.starHandler))
entities.Delete("/star/:uid", middleware.ReqSignedIn, routing.Wrap(s.unstarHandler)) entities.Delete("/star/:uid", middleware.ReqSignedIn, routing.Wrap(s.unstarHandler))
entities.Patch("/:uid", middleware.ReqSignedIn, routing.Wrap(s.patchCommentHandler)) entities.Patch("/:uid", middleware.ReqSignedIn, routing.Wrap(s.patchCommentHandler))
// Remove migrate endpoint in Grafana v10 as breaking change
entities.Post("/migrate", middleware.ReqSignedIn, routing.Wrap(s.migrateHandler))
}) })
} }
@ -189,31 +187,6 @@ func (s *QueryHistoryService) unstarHandler(c *contextmodel.ReqContext) response
return response.JSON(http.StatusOK, QueryHistoryResponse{Result: query}) return response.JSON(http.StatusOK, QueryHistoryResponse{Result: query})
} }
// swagger:route POST /query-history/migrate query_history migrateQueries
//
// Migrate queries to query history.
//
// Adds multiple queries to query history.
//
// Responses:
// 200: getQueryHistoryMigrationResponse
// 400: badRequestError
// 401: unauthorisedError
// 500: internalServerError
func (s *QueryHistoryService) migrateHandler(c *contextmodel.ReqContext) response.Response {
cmd := MigrateQueriesToQueryHistoryCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
totalCount, starredCount, err := s.MigrateQueriesToQueryHistory(c.Req.Context(), c.SignedInUser, cmd)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to migrate query history", err)
}
return response.JSON(http.StatusOK, QueryHistoryMigrationResponse{Message: "Query history successfully migrated", TotalCount: totalCount, StarredCount: starredCount})
}
// swagger:parameters starQuery patchQueryComment deleteQuery unstarQuery // swagger:parameters starQuery patchQueryComment deleteQuery unstarQuery
type QueryHistoryByUID struct { type QueryHistoryByUID struct {
// in:path // in:path
@ -275,13 +248,6 @@ type PatchQueryCommentParams struct {
Body PatchQueryCommentInQueryHistoryCommand `json:"body"` Body PatchQueryCommentInQueryHistoryCommand `json:"body"`
} }
// swagger:parameters migrateQueries
type MigrateQueriesParams struct {
// in:body
// required:true
Body MigrateQueriesToQueryHistoryCommand `json:"body"`
}
//swagger:response getQueryHistorySearchResponse //swagger:response getQueryHistorySearchResponse
type GetQueryHistorySearchResponse struct { type GetQueryHistorySearchResponse struct {
// in: body // in: body
@ -299,9 +265,3 @@ type GetQueryHistoryDeleteQueryResponse struct {
// in: body // in: body
Body QueryHistoryDeleteQueryResponse `json:"body"` Body QueryHistoryDeleteQueryResponse `json:"body"`
} }
// swagger:response getQueryHistoryMigrationResponse
type GetQueryHistoryMigrationResponse struct {
// in: body
Body QueryHistoryMigrationResponse `json:"body"`
}

View File

@ -2,7 +2,6 @@ package queryhistory
import ( import (
"context" "context"
"fmt"
"strconv" "strconv"
"github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db"
@ -271,65 +270,6 @@ func (s QueryHistoryService) unstarQuery(ctx context.Context, user *user.SignedI
return dto, nil return dto, nil
} }
// migrateQueries adds multiple queries into query history
func (s QueryHistoryService) migrateQueries(ctx context.Context, usr *user.SignedInUser, cmd MigrateQueriesToQueryHistoryCommand) (int, int, error) {
queryHistories := make([]*QueryHistory, 0, len(cmd.Queries))
starredQueries := make([]*QueryHistoryStar, 0)
err := s.store.WithTransactionalDbSession(ctx, func(session *db.Session) error {
for _, query := range cmd.Queries {
uid := util.GenerateShortUID()
queryHistories = append(queryHistories, &QueryHistory{
OrgID: usr.OrgID,
UID: uid,
Queries: query.Queries,
DatasourceUID: query.DatasourceUID,
CreatedBy: usr.UserID,
CreatedAt: query.CreatedAt,
Comment: query.Comment,
})
if query.Starred {
starredQueries = append(starredQueries, &QueryHistoryStar{
UserID: usr.UserID,
QueryUID: uid,
})
}
}
batchSize := 50
var err error
for i := 0; i < len(queryHistories); i += batchSize {
j := i + batchSize
if j > len(queryHistories) {
j = len(queryHistories)
}
_, err = session.InsertMulti(queryHistories[i:j])
if err != nil {
return err
}
}
for i := 0; i < len(starredQueries); i += batchSize {
j := i + batchSize
if j > len(starredQueries) {
j = len(starredQueries)
}
_, err = session.InsertMulti(starredQueries[i:j])
if err != nil {
return err
}
}
return err
})
if err != nil {
return 0, 0, fmt.Errorf("failed to migrate query history: %w", err)
}
return len(queryHistories), len(starredQueries), nil
}
func (s QueryHistoryService) deleteStaleQueries(ctx context.Context, olderThan int64) (int, error) { func (s QueryHistoryService) deleteStaleQueries(ctx context.Context, olderThan int64) (int, error) {
var rowsCount int64 var rowsCount int64

View File

@ -74,20 +74,6 @@ type QueryHistoryDeleteQueryResponse struct {
Message string `json:"message"` Message string `json:"message"`
} }
type QueryToMigrate struct {
DatasourceUID string `json:"datasourceUid"`
Queries *simplejson.Json `json:"queries"`
CreatedAt int64 `json:"createdAt"`
Comment string `json:"comment"`
Starred bool `json:"starred"`
}
type QueryHistoryMigrationResponse struct {
Message string `json:"message"`
TotalCount int `json:"totalCount"`
StarredCount int `json:"starredCount"`
}
// CreateQueryInQueryHistoryCommand is the command for adding query history // CreateQueryInQueryHistoryCommand is the command for adding query history
// swagger:model // swagger:model
type CreateQueryInQueryHistoryCommand struct { type CreateQueryInQueryHistoryCommand struct {
@ -105,10 +91,3 @@ type PatchQueryCommentInQueryHistoryCommand struct {
// Updated comment // Updated comment
Comment string `json:"comment"` Comment string `json:"comment"`
} }
// MigrateQueriesToQueryHistoryCommand is the command used for migration of old queries into query history
// swagger:model
type MigrateQueriesToQueryHistoryCommand struct {
// Array of queries to store in query history.
Queries []QueryToMigrate `json:"queries"`
}

View File

@ -35,7 +35,6 @@ type Service interface {
PatchQueryCommentInQueryHistory(ctx context.Context, user *user.SignedInUser, UID string, cmd PatchQueryCommentInQueryHistoryCommand) (QueryHistoryDTO, error) PatchQueryCommentInQueryHistory(ctx context.Context, user *user.SignedInUser, UID string, cmd PatchQueryCommentInQueryHistoryCommand) (QueryHistoryDTO, error)
StarQueryInQueryHistory(ctx context.Context, user *user.SignedInUser, UID string) (QueryHistoryDTO, error) StarQueryInQueryHistory(ctx context.Context, user *user.SignedInUser, UID string) (QueryHistoryDTO, error)
UnstarQueryInQueryHistory(ctx context.Context, user *user.SignedInUser, UID string) (QueryHistoryDTO, error) UnstarQueryInQueryHistory(ctx context.Context, user *user.SignedInUser, UID string) (QueryHistoryDTO, error)
MigrateQueriesToQueryHistory(ctx context.Context, user *user.SignedInUser, cmd MigrateQueriesToQueryHistoryCommand) (int, int, error)
DeleteStaleQueriesInQueryHistory(ctx context.Context, olderThan int64) (int, error) DeleteStaleQueriesInQueryHistory(ctx context.Context, olderThan int64) (int, error)
EnforceRowLimitInQueryHistory(ctx context.Context, limit int, starredQueries bool) (int, error) EnforceRowLimitInQueryHistory(ctx context.Context, limit int, starredQueries bool) (int, error)
} }
@ -72,10 +71,6 @@ func (s QueryHistoryService) UnstarQueryInQueryHistory(ctx context.Context, user
return s.unstarQuery(ctx, user, UID) return s.unstarQuery(ctx, user, UID)
} }
func (s QueryHistoryService) MigrateQueriesToQueryHistory(ctx context.Context, user *user.SignedInUser, cmd MigrateQueriesToQueryHistoryCommand) (int, int, error) {
return s.migrateQueries(ctx, user, cmd)
}
func (s QueryHistoryService) DeleteStaleQueriesInQueryHistory(ctx context.Context, olderThan int64) (int, error) { func (s QueryHistoryService) DeleteStaleQueriesInQueryHistory(ctx context.Context, olderThan int64) (int, error) {
return s.deleteStaleQueries(ctx, olderThan) return s.deleteStaleQueries(ctx, olderThan)
} }

View File

@ -1,120 +0,0 @@
package queryhistory
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/components/simplejson"
)
func TestIntegrationMigrateQueriesToQueryHistory(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
testScenario(t, "When users tries to migrate 1 query in query history it should succeed",
func(t *testing.T, sc scenarioContext) {
command := MigrateQueriesToQueryHistoryCommand{
Queries: []QueryToMigrate{
{
DatasourceUID: "NCzh67i",
Queries: simplejson.NewFromAny(map[string]interface{}{
"expr": "test",
}),
Comment: "",
Starred: false,
CreatedAt: sc.service.now().Unix(),
},
},
}
sc.reqContext.Req.Body = mockRequestBody(command)
resp := sc.service.migrateHandler(sc.reqContext)
var response QueryHistoryMigrationResponse
err := json.Unmarshal(resp.Body(), &response)
require.NoError(t, err)
require.Equal(t, 200, resp.Status())
require.Equal(t, "Query history successfully migrated", response.Message)
require.Equal(t, 1, response.TotalCount)
require.Equal(t, 0, response.StarredCount)
})
testScenario(t, "When users tries to migrate multiple queries in query history it should succeed",
func(t *testing.T, sc scenarioContext) {
command := MigrateQueriesToQueryHistoryCommand{
Queries: []QueryToMigrate{
{
DatasourceUID: "NCzh67i",
Queries: simplejson.NewFromAny(map[string]interface{}{
"expr": "test1",
}),
Comment: "",
Starred: false,
CreatedAt: sc.service.now().Unix(),
},
{
DatasourceUID: "NCzh67i",
Queries: simplejson.NewFromAny(map[string]interface{}{
"expr": "test2",
}),
Comment: "",
Starred: false,
CreatedAt: sc.service.now().Unix() - int64(100),
},
{
DatasourceUID: "ABch68f",
Queries: simplejson.NewFromAny(map[string]interface{}{
"expr": "test3",
}),
Comment: "",
Starred: false,
CreatedAt: sc.service.now().Unix() - int64(1000),
},
},
}
sc.reqContext.Req.Body = mockRequestBody(command)
resp := sc.service.migrateHandler(sc.reqContext)
var response QueryHistoryMigrationResponse
err := json.Unmarshal(resp.Body(), &response)
require.NoError(t, err)
require.Equal(t, 200, resp.Status())
require.Equal(t, "Query history successfully migrated", response.Message)
require.Equal(t, 3, response.TotalCount)
require.Equal(t, 0, response.StarredCount)
})
testScenario(t, "When users tries to migrate starred and not starred query in query history it should succeed",
func(t *testing.T, sc scenarioContext) {
command := MigrateQueriesToQueryHistoryCommand{
Queries: []QueryToMigrate{
{
DatasourceUID: "NCzh67i",
Queries: simplejson.NewFromAny(map[string]interface{}{
"expr": "test1",
}),
Comment: "",
Starred: true,
CreatedAt: sc.service.now().Unix(),
},
{
DatasourceUID: "NCzh67i",
Queries: simplejson.NewFromAny(map[string]interface{}{
"expr": "test2",
}),
Comment: "",
Starred: false,
CreatedAt: sc.service.now().Unix() - int64(100),
},
},
}
sc.reqContext.Req.Body = mockRequestBody(command)
resp := sc.service.migrateHandler(sc.reqContext)
var response QueryHistoryMigrationResponse
err := json.Unmarshal(resp.Body(), &response)
require.NoError(t, err)
require.Equal(t, 200, resp.Status())
require.Equal(t, "Query history successfully migrated", response.Message)
require.Equal(t, 2, response.TotalCount)
require.Equal(t, 1, response.StarredCount)
})
}

View File

@ -7736,40 +7736,6 @@
} }
} }
}, },
"/query-history/migrate": {
"post": {
"description": "Adds multiple queries to query history.",
"tags": [
"query_history"
],
"summary": "Migrate queries to query history.",
"operationId": "migrateQueries",
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/MigrateQueriesToQueryHistoryCommand"
}
}
],
"responses": {
"200": {
"$ref": "#/responses/getQueryHistoryMigrationResponse"
},
"400": {
"$ref": "#/responses/badRequestError"
},
"401": {
"$ref": "#/responses/unauthorisedError"
},
"500": {
"$ref": "#/responses/internalServerError"
}
}
}
},
"/query-history/star/{query_history_uid}": { "/query-history/star/{query_history_uid}": {
"post": { "post": {
"description": "Adds star to query in query history as specified by the UID.", "description": "Adds star to query in query history as specified by the UID.",
@ -15042,19 +15008,6 @@
} }
} }
}, },
"MigrateQueriesToQueryHistoryCommand": {
"description": "MigrateQueriesToQueryHistoryCommand is the command used for migration of old queries into query history",
"type": "object",
"properties": {
"queries": {
"description": "Array of queries to store in query history.",
"type": "array",
"items": {
"$ref": "#/definitions/QueryToMigrate"
}
}
}
},
"MoveFolderCommand": { "MoveFolderCommand": {
"description": "MoveFolderCommand captures the information required by the folder service\nto move a folder.", "description": "MoveFolderCommand captures the information required by the folder service\nto move a folder.",
"type": "object", "type": "object",
@ -16406,22 +16359,6 @@
} }
} }
}, },
"QueryHistoryMigrationResponse": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"starredCount": {
"type": "integer",
"format": "int64"
},
"totalCount": {
"type": "integer",
"format": "int64"
}
}
},
"QueryHistoryPreference": { "QueryHistoryPreference": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -16554,27 +16491,6 @@
} }
} }
}, },
"QueryToMigrate": {
"type": "object",
"properties": {
"comment": {
"type": "string"
},
"createdAt": {
"type": "integer",
"format": "int64"
},
"datasourceUid": {
"type": "string"
},
"queries": {
"$ref": "#/definitions/Json"
},
"starred": {
"type": "boolean"
}
}
},
"QuotaDTO": { "QuotaDTO": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -173,18 +173,6 @@ describe('RichHistoryRemoteStorage', () => {
} as Partial<UserPreferencesDTO>); } as Partial<UserPreferencesDTO>);
}); });
it('migrates provided rich history items', async () => {
const { richHistoryQuery, dto } = setup();
fetchMock.mockReturnValue(of({}));
await storage.migrate([richHistoryQuery]);
expect(fetchMock).toBeCalledWith({
url: '/api/query-history/migrate',
method: 'POST',
data: { queries: [dto] },
showSuccessAlert: false,
});
});
it('stars query history items', async () => { it('stars query history items', async () => {
const { richHistoryQuery, dto } = setup(); const { richHistoryQuery, dto } = setup();
postMock.mockResolvedValue({ postMock.mockResolvedValue({

View File

@ -8,7 +8,7 @@ import { PreferencesService } from '../services/PreferencesService';
import { RichHistorySearchFilters, RichHistorySettings, SortOrder } from '../utils/richHistoryTypes'; import { RichHistorySearchFilters, RichHistorySettings, SortOrder } from '../utils/richHistoryTypes';
import RichHistoryStorage, { RichHistoryStorageWarningDetails } from './RichHistoryStorage'; import RichHistoryStorage, { RichHistoryStorageWarningDetails } from './RichHistoryStorage';
import { fromDTO, toDTO } from './remoteStorageConverter'; import { fromDTO } from './remoteStorageConverter';
export type RichHistoryRemoteStorageDTO = { export type RichHistoryRemoteStorageDTO = {
uid: string; uid: string;
@ -19,18 +19,6 @@ export type RichHistoryRemoteStorageDTO = {
queries: DataQuery[]; queries: DataQuery[];
}; };
type RichHistoryRemoteStorageMigrationDTO = {
datasourceUid: string;
queries: DataQuery[];
createdAt: number;
starred: boolean;
comment: string;
};
type RichHistoryRemoteStorageMigrationPayloadDTO = {
queries: RichHistoryRemoteStorageMigrationDTO[];
};
type RichHistoryRemoteStorageResultsPayloadDTO = { type RichHistoryRemoteStorageResultsPayloadDTO = {
result: { result: {
queryHistory: RichHistoryRemoteStorageDTO[]; queryHistory: RichHistoryRemoteStorageDTO[];
@ -122,21 +110,6 @@ export default class RichHistoryRemoteStorage implements RichHistoryStorage {
} }
return fromDTO(dto.result); return fromDTO(dto.result);
} }
/**
* @internal Used only for migration purposes. Will be removed in future.
*/
async migrate(richHistory: RichHistoryQuery[]) {
const data: RichHistoryRemoteStorageMigrationPayloadDTO = { queries: richHistory.map(toDTO) };
await lastValueFrom(
getBackendSrv().fetch({
url: '/api/query-history/migrate',
method: 'POST',
data,
showSuccessAlert: false,
})
);
}
} }
function buildQueryParams(filters: RichHistorySearchFilters): string { function buildQueryParams(filters: RichHistorySearchFilters): string {

View File

@ -96,5 +96,4 @@ export const RICH_HISTORY_SETTING_KEYS = {
starredTabAsFirstTab: 'grafana.explore.richHistory.starredTabAsFirstTab', starredTabAsFirstTab: 'grafana.explore.richHistory.starredTabAsFirstTab',
activeDatasourceOnly: 'grafana.explore.richHistory.activeDatasourceOnly', activeDatasourceOnly: 'grafana.explore.richHistory.activeDatasourceOnly',
datasourceFilters: 'grafana.explore.richHistory.datasourceFilters', datasourceFilters: 'grafana.explore.richHistory.datasourceFilters',
migrated: 'grafana.explore.richHistory.migrated',
}; };

View File

@ -13,9 +13,7 @@ import {
createQueryHeading, createQueryHeading,
deleteAllFromRichHistory, deleteAllFromRichHistory,
deleteQueryInRichHistory, deleteQueryInRichHistory,
migrateQueryHistoryFromLocalStorage,
SortOrder, SortOrder,
LocalStorageMigrationStatus,
} from './richHistory'; } from './richHistory';
const richHistoryStorageMock: RichHistoryStorage = {} as RichHistoryStorage; const richHistoryStorageMock: RichHistoryStorage = {} as RichHistoryStorage;
@ -179,35 +177,6 @@ describe('richHistory', () => {
}); });
}); });
describe('migration', () => {
beforeEach(() => {
richHistoryRemoteStorageMock.migrate.mockReset();
});
it('migrates history', async () => {
const history = { richHistory: [{ id: 'test' }, { id: 'test2' }], total: 2 };
richHistoryLocalStorageMock.getRichHistory.mockReturnValue(history);
const migrationResult = await migrateQueryHistoryFromLocalStorage();
expect(richHistoryRemoteStorageMock.migrate).toBeCalledWith(history.richHistory);
expect(migrationResult.status).toBe(LocalStorageMigrationStatus.Successful);
expect(migrationResult.error).toBeUndefined();
});
it('does not migrate if there are no entries', async () => {
richHistoryLocalStorageMock.getRichHistory.mockReturnValue({ richHistory: [] });
const migrationResult = await migrateQueryHistoryFromLocalStorage();
expect(richHistoryRemoteStorageMock.migrate).not.toBeCalled();
expect(migrationResult.status).toBe(LocalStorageMigrationStatus.NotNeeded);
expect(migrationResult.error).toBeUndefined();
});
it('propagates thrown errors', async () => {
richHistoryLocalStorageMock.getRichHistory.mockRejectedValue(new Error('migration failed'));
const migrationResult = await migrateQueryHistoryFromLocalStorage();
expect(migrationResult.status).toBe(LocalStorageMigrationStatus.Failed);
expect(migrationResult.error?.message).toBe('migration failed');
});
});
describe('mapNumbertoTimeInSlider', () => { describe('mapNumbertoTimeInSlider', () => {
it('should correctly map number to value', () => { it('should correctly map number to value', () => {
const value = mapNumbertoTimeInSlider(25); const value = mapNumbertoTimeInSlider(25);

View File

@ -4,17 +4,11 @@ import { DataQuery, DataSourceApi, dateTimeFormat, ExploreUrlState, urlUtil } fr
import { serializeStateToUrlParam } from '@grafana/data/src/utils/url'; import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
import { getDataSourceSrv } from '@grafana/runtime'; import { getDataSourceSrv } from '@grafana/runtime';
import { notifyApp } from 'app/core/actions'; import { notifyApp } from 'app/core/actions';
import { import { createErrorNotification, createWarningNotification } from 'app/core/copy/appNotification';
createErrorNotification,
createSuccessNotification,
createWarningNotification,
} from 'app/core/copy/appNotification';
import { dispatch } from 'app/store/store'; import { dispatch } from 'app/store/store';
import { RichHistoryQuery } from 'app/types/explore'; import { RichHistoryQuery } from 'app/types/explore';
import { config } from '../config'; import { config } from '../config';
import RichHistoryLocalStorage from '../history/RichHistoryLocalStorage';
import RichHistoryRemoteStorage from '../history/RichHistoryRemoteStorage';
import { import {
RichHistoryResults, RichHistoryResults,
RichHistoryServiceError, RichHistoryServiceError,
@ -134,43 +128,6 @@ export async function deleteQueryInRichHistory(id: string) {
} }
} }
export enum LocalStorageMigrationStatus {
Successful = 'successful',
Failed = 'failed',
NotNeeded = 'not-needed',
}
export interface LocalStorageMigrationResult {
status: LocalStorageMigrationStatus;
error?: Error;
}
export async function migrateQueryHistoryFromLocalStorage(): Promise<LocalStorageMigrationResult> {
const richHistoryLocalStorage = new RichHistoryLocalStorage();
const richHistoryRemoteStorage = new RichHistoryRemoteStorage();
try {
const { richHistory } = await richHistoryLocalStorage.getRichHistory({
datasourceFilters: [],
from: 0,
search: '',
sortOrder: SortOrder.Descending,
starred: false,
to: 14,
});
if (richHistory.length === 0) {
return { status: LocalStorageMigrationStatus.NotNeeded };
}
await richHistoryRemoteStorage.migrate(richHistory);
dispatch(notifyApp(createSuccessNotification('Query history successfully migrated from local storage')));
return { status: LocalStorageMigrationStatus.Successful };
} catch (error) {
const errorToThrow = error instanceof Error ? error : new Error('Uknown error occurred.');
dispatch(notifyApp(createWarningNotification(`Query history migration failed. ${errorToThrow.message}`)));
return { status: LocalStorageMigrationStatus.Failed, error: errorToThrow };
}
}
export const createUrlFromRichHistory = (query: RichHistoryQuery) => { export const createUrlFromRichHistory = (query: RichHistoryQuery) => {
const exploreState: ExploreUrlState = { const exploreState: ExploreUrlState = {
/* Default range, as we are not saving timerange in rich history */ /* Default range, as we are not saving timerange in rich history */

View File

@ -61,7 +61,6 @@ function setup(queries: DataQuery[]) {
right: undefined, right: undefined,
richHistoryStorageFull: false, richHistoryStorageFull: false,
richHistoryLimitExceededWarningShown: false, richHistoryLimitExceededWarningShown: false,
richHistoryMigrationFailed: false,
}; };
const store = configureStore({ explore: initialState, user: { orgId: 1 } as UserState }); const store = configureStore({ explore: initialState, user: { orgId: 1 } as UserState });

View File

@ -22,8 +22,6 @@ import { lastUsedDatasourceKeyForOrgId } from 'app/core/utils/explore';
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
import { configureStore } from 'app/store/configureStore'; import { configureStore } from 'app/store/configureStore';
import { RICH_HISTORY_KEY, RichHistoryLocalStorageDTO } from '../../../../core/history/RichHistoryLocalStorage';
import { RICH_HISTORY_SETTING_KEYS } from '../../../../core/history/richHistoryLocalStorageUtils';
import { LokiDatasource } from '../../../../plugins/datasource/loki/datasource'; import { LokiDatasource } from '../../../../plugins/datasource/loki/datasource';
import { LokiQuery } from '../../../../plugins/datasource/loki/types'; import { LokiQuery } from '../../../../plugins/datasource/loki/types';
import { ExploreId } from '../../../../types'; import { ExploreId } from '../../../../types';
@ -196,18 +194,3 @@ export const withinExplore = (exploreId: ExploreId) => {
const container = screen.getAllByTestId('data-testid Explore'); const container = screen.getAllByTestId('data-testid Explore');
return within(container[exploreId === ExploreId.left ? 0 : 1]); return within(container[exploreId === ExploreId.left ? 0 : 1]);
}; };
export const localStorageHasAlreadyBeenMigrated = () => {
window.localStorage.setItem(RICH_HISTORY_SETTING_KEYS.migrated, 'true');
};
export const setupLocalStorageRichHistory = (dsName: string) => {
const richHistoryDTO: RichHistoryLocalStorageDTO = {
ts: Date.now(),
datasourceName: dsName,
starred: true,
comment: '',
queries: [{ refId: 'A' }],
};
window.localStorage.setItem(RICH_HISTORY_KEY, JSON.stringify([richHistoryDTO]));
};

View File

@ -31,13 +31,7 @@ import {
switchToQueryHistoryTab, switchToQueryHistoryTab,
} from './helper/interactions'; } from './helper/interactions';
import { makeLogsQueryResponse } from './helper/query'; import { makeLogsQueryResponse } from './helper/query';
import { import { setupExplore, tearDown, waitForExplore } from './helper/setup';
localStorageHasAlreadyBeenMigrated,
setupExplore,
setupLocalStorageRichHistory,
tearDown,
waitForExplore,
} from './helper/setup';
const fetchMock = jest.fn(); const fetchMock = jest.fn();
const postMock = jest.fn(); const postMock = jest.fn();
@ -225,56 +219,8 @@ describe('Explore: Query History', () => {
assertDataSourceFilterVisibility(false); assertDataSourceFilterVisibility(false);
}); });
describe('local storage migration', () => {
it('does not migrate if query history is not enabled', async () => {
config.queryHistoryEnabled = false;
const { datasources } = setupExplore();
setupLocalStorageRichHistory('loki');
(datasources.loki.query as jest.Mock).mockReturnValueOnce(makeLogsQueryResponse());
getMock.mockReturnValue({ result: { queryHistory: [] } });
await waitForExplore();
await openQueryHistory();
expect(postMock).not.toBeCalledWith('/api/query-history/migrate', { queries: [] });
expect(reportInteractionMock).toBeCalledWith('grafana_explore_query_history_opened', {
queryHistoryEnabled: false,
});
});
it('migrates query history from local storage', async () => {
config.queryHistoryEnabled = true;
const { datasources } = setupExplore();
setupLocalStorageRichHistory('loki');
(datasources.loki.query as jest.Mock).mockReturnValueOnce(makeLogsQueryResponse());
fetchMock.mockReturnValue(of({ data: { result: { queryHistory: [] } } }));
await waitForExplore();
await openQueryHistory();
expect(fetchMock).toBeCalledWith(
expect.objectContaining({
url: expect.stringMatching('/api/query-history/migrate'),
data: { queries: [expect.objectContaining({ datasourceUid: 'loki-uid' })] },
})
);
fetchMock.mockReset();
fetchMock.mockReturnValue(of({ data: { result: { queryHistory: [] } } }));
await closeQueryHistory();
await openQueryHistory();
expect(fetchMock).not.toBeCalledWith(
expect.objectContaining({
url: expect.stringMatching('/api/query-history/migrate'),
})
);
expect(reportInteractionMock).toBeCalledWith('grafana_explore_query_history_opened', {
queryHistoryEnabled: true,
});
});
});
it('pagination', async () => { it('pagination', async () => {
config.queryHistoryEnabled = true; config.queryHistoryEnabled = true;
localStorageHasAlreadyBeenMigrated();
const { datasources } = setupExplore(); const { datasources } = setupExplore();
(datasources.loki.query as jest.Mock).mockReturnValueOnce(makeLogsQueryResponse()); (datasources.loki.query as jest.Mock).mockReturnValueOnce(makeLogsQueryResponse());
fetchMock.mockReturnValue( fetchMock.mockReturnValue(

View File

@ -1,18 +1,13 @@
import { AnyAction, createAction } from '@reduxjs/toolkit'; import { AnyAction, createAction } from '@reduxjs/toolkit';
import { HistoryItem } from '@grafana/data'; import { HistoryItem } from '@grafana/data';
import { config, logError } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema'; import { DataQuery } from '@grafana/schema';
import { RICH_HISTORY_SETTING_KEYS } from 'app/core/history/richHistoryLocalStorageUtils';
import store from 'app/core/store';
import { import {
addToRichHistory, addToRichHistory,
deleteAllFromRichHistory, deleteAllFromRichHistory,
deleteQueryInRichHistory, deleteQueryInRichHistory,
getRichHistory, getRichHistory,
getRichHistorySettings, getRichHistorySettings,
LocalStorageMigrationStatus,
migrateQueryHistoryFromLocalStorage,
updateCommentInRichHistory, updateCommentInRichHistory,
updateRichHistorySettings, updateRichHistorySettings,
updateStarredInRichHistory, updateStarredInRichHistory,
@ -24,7 +19,6 @@ import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/uti
import { import {
richHistoryLimitExceededAction, richHistoryLimitExceededAction,
richHistoryMigrationFailedAction,
richHistorySearchFiltersUpdatedAction, richHistorySearchFiltersUpdatedAction,
richHistorySettingsUpdatedAction, richHistorySettingsUpdatedAction,
richHistoryStorageFullAction, richHistoryStorageFullAction,
@ -173,21 +167,6 @@ export const clearRichHistoryResults = (exploreId: ExploreId): ThunkResult<void>
*/ */
export const initRichHistory = (): ThunkResult<void> => { export const initRichHistory = (): ThunkResult<void> => {
return async (dispatch, getState) => { return async (dispatch, getState) => {
const queriesMigrated = store.getBool(RICH_HISTORY_SETTING_KEYS.migrated, false);
const migrationFailedDuringThisSession = getState().explore.richHistoryMigrationFailed;
// Query history migration should always be successful, but in case of unexpected errors we ensure
// the migration attempt happens only once per session, and the user is informed about the failure
// in a way that can help with potential investigation.
if (config.queryHistoryEnabled && !queriesMigrated && !migrationFailedDuringThisSession) {
const migrationResult = await migrateQueryHistoryFromLocalStorage();
if (migrationResult.status === LocalStorageMigrationStatus.Failed) {
dispatch(richHistoryMigrationFailedAction());
logError(migrationResult.error!, { explore: { event: 'QueryHistoryMigrationFailed' } });
} else {
store.set(RICH_HISTORY_SETTING_KEYS.migrated, true);
}
}
let settings = getState().explore.richHistorySettings; let settings = getState().explore.richHistorySettings;
if (!settings) { if (!settings) {
settings = await getRichHistorySettings(); settings = await getRichHistorySettings();

View File

@ -31,7 +31,6 @@ export const richHistoryUpdatedAction = createAction<{ richHistoryResults: RichH
); );
export const richHistoryStorageFullAction = createAction('explore/richHistoryStorageFullAction'); export const richHistoryStorageFullAction = createAction('explore/richHistoryStorageFullAction');
export const richHistoryLimitExceededAction = createAction('explore/richHistoryLimitExceededAction'); export const richHistoryLimitExceededAction = createAction('explore/richHistoryLimitExceededAction');
export const richHistoryMigrationFailedAction = createAction('explore/richHistoryMigrationFailedAction');
export const richHistorySettingsUpdatedAction = createAction<RichHistorySettings>('explore/richHistorySettingsUpdated'); export const richHistorySettingsUpdatedAction = createAction<RichHistorySettings>('explore/richHistorySettingsUpdated');
export const richHistorySearchFiltersUpdatedAction = createAction<{ export const richHistorySearchFiltersUpdatedAction = createAction<{
@ -175,7 +174,6 @@ export const initialExploreState: ExploreState = {
correlations: undefined, correlations: undefined,
richHistoryStorageFull: false, richHistoryStorageFull: false,
richHistoryLimitExceededWarningShown: false, richHistoryLimitExceededWarningShown: false,
richHistoryMigrationFailed: false,
largerExploreId: undefined, largerExploreId: undefined,
maxedExploreId: undefined, maxedExploreId: undefined,
evenSplitPanes: true, evenSplitPanes: true,
@ -256,13 +254,6 @@ export const exploreReducer = (state = initialExploreState, action: AnyAction):
}; };
} }
if (richHistoryMigrationFailedAction.match(action)) {
return {
...state,
richHistoryMigrationFailed: true,
};
}
if (resetExploreAction.match(action)) { if (resetExploreAction.match(action)) {
const leftState = state[ExploreId.left]; const leftState = state[ExploreId.left];
const rightState = state[ExploreId.right]; const rightState = state[ExploreId.right];

View File

@ -65,11 +65,6 @@ export interface ExploreState {
*/ */
richHistoryLimitExceededWarningShown: boolean; richHistoryLimitExceededWarningShown: boolean;
/**
* True if a warning message about failed rich history has been shown already in this session.
*/
richHistoryMigrationFailed: boolean;
/** /**
* On a split manual resize, we calculate which pane is larger, or if they are roughly the same size. If undefined, it is not split or they are roughly the same size * On a split manual resize, we calculate which pane is larger, or if they are roughly the same size. If undefined, it is not split or they are roughly the same size
*/ */

View File

@ -6109,19 +6109,6 @@
], ],
"type": "object" "type": "object"
}, },
"MigrateQueriesToQueryHistoryCommand": {
"description": "MigrateQueriesToQueryHistoryCommand is the command used for migration of old queries into query history",
"properties": {
"queries": {
"description": "Array of queries to store in query history.",
"items": {
"$ref": "#/components/schemas/QueryToMigrate"
},
"type": "array"
}
},
"type": "object"
},
"MoveFolderCommand": { "MoveFolderCommand": {
"description": "MoveFolderCommand captures the information required by the folder service\nto move a folder.", "description": "MoveFolderCommand captures the information required by the folder service\nto move a folder.",
"properties": { "properties": {
@ -7472,22 +7459,6 @@
}, },
"type": "object" "type": "object"
}, },
"QueryHistoryMigrationResponse": {
"properties": {
"message": {
"type": "string"
},
"starredCount": {
"format": "int64",
"type": "integer"
},
"totalCount": {
"format": "int64",
"type": "integer"
}
},
"type": "object"
},
"QueryHistoryPreference": { "QueryHistoryPreference": {
"properties": { "properties": {
"homeTab": { "homeTab": {
@ -19262,41 +19233,6 @@
] ]
} }
}, },
"/query-history/migrate": {
"post": {
"description": "Adds multiple queries to query history.",
"operationId": "migrateQueries",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MigrateQueriesToQueryHistoryCommand"
}
}
},
"required": true,
"x-originalParamName": "body"
},
"responses": {
"200": {
"$ref": "#/components/responses/getQueryHistoryMigrationResponse"
},
"400": {
"$ref": "#/components/responses/badRequestError"
},
"401": {
"$ref": "#/components/responses/unauthorisedError"
},
"500": {
"$ref": "#/components/responses/internalServerError"
}
},
"summary": "Migrate queries to query history.",
"tags": [
"query_history"
]
}
},
"/query-history/star/{query_history_uid}": { "/query-history/star/{query_history_uid}": {
"delete": { "delete": {
"description": "Removes star from query in query history as specified by the UID.", "description": "Removes star from query in query history as specified by the UID.",