[data.search.session] Include version in search session for restore warnings (#105528)

* [data.search.session] Include version in search session saved object for warnings

* Don't show version message if not restorable

* Fix build

* fix ts, fix jest, add migration

* tests ts

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Anton Dosov <anton.dosov@elastic.co>
This commit is contained in:
Lukas Olson 2021-07-21 09:01:02 -07:00 committed by GitHub
parent a6af9d5050
commit 8f791ef8fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 223 additions and 66 deletions

View file

@ -71,6 +71,10 @@ export interface SearchSessionSavedObjectAttributes {
realmType?: string;
realmName?: string;
username?: string;
/**
* Version information to display warnings when trying to restore a session from a different version
*/
version: string;
}
export interface SearchSessionRequestInfo {

View file

@ -24,6 +24,7 @@ const mockSavedObject: SearchSessionSavedObject = {
expires: new Date().toISOString(),
status: SearchSessionStatus.COMPLETE,
persisted: true,
version: '8.0.0',
},
references: [],
};

View file

@ -33,6 +33,7 @@ const mockSavedObject: SearchSessionSavedObject = {
expires: new Date().toISOString(),
status: SearchSessionStatus.COMPLETE,
persisted: true,
version: '8.0.0',
},
references: [],
};

View file

@ -51,7 +51,12 @@ export class DataEnhancedPlugin
this.config = this.initializerContext.config.get<ConfigSchema>();
if (this.config.search.sessions.enabled) {
const sessionsConfig = this.config.search.sessions;
registerSearchSessionsMgmt(core, sessionsConfig, { data, management });
registerSearchSessionsMgmt(
core,
sessionsConfig,
this.initializerContext.env.packageInfo.version,
{ data, management }
);
}
this.usageCollector = data.search.usageCollector;

View file

@ -22,6 +22,7 @@ export class SearchSessionsMgmtApp {
constructor(
private coreSetup: CoreSetup<IManagementSectionsPluginsStart>,
private config: SessionsConfigSchema,
private kibanaVersion: string,
private params: ManagementAppMountParams,
private pluginsSetup: IManagementSectionsPluginsSetup
) {}
@ -65,6 +66,7 @@ export class SearchSessionsMgmtApp {
i18n,
uiSettings,
share,
kibanaVersion: this.kibanaVersion,
};
const { element } = params;

View file

@ -82,6 +82,7 @@ describe('Background Search Session Management Main', () => {
timezone="UTC"
documentation={new AsyncSearchIntroDocumentation(docLinks)}
config={mockConfig}
kibanaVersion={'8.0.0'}
/>
</LocaleWrapper>
);

View file

@ -23,6 +23,7 @@ interface Props {
timezone: string;
config: SessionsConfigSchema;
plugins: IManagementSectionsPluginsSetup;
kibanaVersion: string;
}
export function SearchSessionsMgmtMain({ documentation, ...tableProps }: Props) {

View file

@ -34,6 +34,7 @@ describe('Background Search Session management status labels', () => {
expires: '2020-12-07T00:19:32Z',
initialState: {},
restoreState: {},
version: '8.0.0',
};
});

View file

@ -91,6 +91,7 @@ describe('Background Search Session Management Table', () => {
api={api}
timezone="UTC"
config={mockConfig}
kibanaVersion={'8.0.0'}
/>
</LocaleWrapper>
);
@ -123,6 +124,7 @@ describe('Background Search Session Management Table', () => {
api={api}
timezone="UTC"
config={mockConfig}
kibanaVersion={'8.0.0'}
/>
</LocaleWrapper>
);
@ -132,7 +134,7 @@ describe('Background Search Session Management Table', () => {
expect(table.find('tbody td').map((node) => node.text())).toMatchInlineSnapshot(`
Array [
"App",
"Namevery background search ",
"Namevery background search ",
"# Searches0",
"StatusExpired",
"Created2 Dec, 2020, 00:19:32",
@ -166,6 +168,7 @@ describe('Background Search Session Management Table', () => {
api={api}
timezone="UTC"
config={mockConfig}
kibanaVersion={'8.0.0'}
/>
</LocaleWrapper>
);
@ -199,6 +202,7 @@ describe('Background Search Session Management Table', () => {
api={api}
timezone="UTC"
config={mockConfig}
kibanaVersion={'8.0.0'}
/>
</LocaleWrapper>
);

View file

@ -28,9 +28,18 @@ interface Props {
timezone: string;
config: SessionsConfigSchema;
plugins: IManagementSectionsPluginsSetup;
kibanaVersion: string;
}
export function SearchSessionsMgmtTable({ core, api, timezone, config, plugins, ...props }: Props) {
export function SearchSessionsMgmtTable({
core,
api,
timezone,
config,
plugins,
kibanaVersion,
...props
}: Props) {
const [tableData, setTableData] = useState<UISession[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [debouncedIsLoading, setDebouncedIsLoading] = useState(false);
@ -111,7 +120,7 @@ export function SearchSessionsMgmtTable({ core, api, timezone, config, plugins,
rowProps={() => ({
'data-test-subj': 'searchSessionsRow',
})}
columns={getColumns(core, plugins, api, config, timezone, onActionComplete)}
columns={getColumns(core, plugins, api, config, timezone, onActionComplete, kibanaVersion)}
items={tableData}
pagination={pagination}
search={search}

View file

@ -37,6 +37,7 @@ export interface AppDependencies {
http: HttpStart;
i18n: I18nStart;
config: SessionsConfigSchema;
kibanaVersion: string;
}
export const APP = {
@ -52,6 +53,7 @@ export type SessionsConfigSchema = ConfigSchema['search']['sessions'];
export function registerSearchSessionsMgmt(
coreSetup: CoreSetup<DataEnhancedStartDependencies>,
config: SessionsConfigSchema,
kibanaVersion: string,
services: IManagementSectionsPluginsSetup
) {
services.management.sections.section.kibana.registerApp({
@ -60,7 +62,7 @@ export function registerSearchSessionsMgmt(
order: 1.75,
mount: async (params) => {
const { SearchSessionsMgmtApp: MgmtApp } = await import('./application');
const mgmtApp = new MgmtApp(coreSetup, config, params, services);
const mgmtApp = new MgmtApp(coreSetup, config, kibanaVersion, params, services);
return mgmtApp.mountManagementSection();
},
});

View file

@ -84,6 +84,7 @@ describe('Search Sessions Management API', () => {
"restoreState": Object {},
"restoreUrl": "hello-cool-undefined-url",
"status": "complete",
"version": undefined,
},
]
`);

View file

@ -91,6 +91,7 @@ const mapToUISession = (urls: UrlGeneratorsStart, config: SessionsConfigSchema)
initialState,
restoreState,
idMapping,
version,
} = savedObject.attributes;
const status = getUIStatus(savedObject.attributes);
@ -115,6 +116,7 @@ const mapToUISession = (urls: UrlGeneratorsStart, config: SessionsConfigSchema)
initialState,
restoreState,
numSearches: Object.keys(idMapping).length,
version,
};
};

View file

@ -76,11 +76,20 @@ describe('Search Sessions Management table column factory', () => {
expires: '2020-12-07T00:19:32Z',
initialState: {},
restoreState: {},
version: '7.14.0',
};
});
test('returns columns', () => {
const columns = getColumns(mockCoreStart, mockPluginsSetup, api, mockConfig, tz, handleAction);
const columns = getColumns(
mockCoreStart,
mockPluginsSetup,
api,
mockConfig,
tz,
handleAction,
'7.14.0'
);
expect(columns).toMatchInlineSnapshot(`
Array [
Object {
@ -144,7 +153,8 @@ describe('Search Sessions Management table column factory', () => {
api,
mockConfig,
tz,
handleAction
handleAction,
'7.14.0'
) as Array<EuiTableFieldDataColumnType<UISession>>;
const name = mount(nameColumn.render!(mockSession.name, mockSession) as ReactElement);
@ -162,7 +172,8 @@ describe('Search Sessions Management table column factory', () => {
api,
mockConfig,
tz,
handleAction
handleAction,
'7.14.0'
) as Array<EuiTableFieldDataColumnType<UISession>>;
const numOfSearchesLine = mount(
@ -181,7 +192,8 @@ describe('Search Sessions Management table column factory', () => {
api,
mockConfig,
tz,
handleAction
handleAction,
'7.14.0'
) as Array<EuiTableFieldDataColumnType<UISession>>;
const statusLine = mount(status.render!(mockSession.status, mockSession) as ReactElement);
@ -197,7 +209,8 @@ describe('Search Sessions Management table column factory', () => {
api,
mockConfig,
tz,
handleAction
handleAction,
'7.14.0'
) as Array<EuiTableFieldDataColumnType<UISession>>;
mockSession.status = 'INVALID' as SearchSessionStatus;
@ -220,7 +233,8 @@ describe('Search Sessions Management table column factory', () => {
api,
mockConfig,
tz,
handleAction
handleAction,
'7.14.0'
) as Array<EuiTableFieldDataColumnType<UISession>>;
const date = mount(createdDateCol.render!(mockSession.created, mockSession) as ReactElement);
@ -237,7 +251,8 @@ describe('Search Sessions Management table column factory', () => {
api,
mockConfig,
tz,
handleAction
handleAction,
'7.14.0'
) as Array<EuiTableFieldDataColumnType<UISession>>;
const date = mount(createdDateCol.render!(mockSession.created, mockSession) as ReactElement);
@ -252,7 +267,8 @@ describe('Search Sessions Management table column factory', () => {
api,
mockConfig,
tz,
handleAction
handleAction,
'7.14.0'
) as Array<EuiTableFieldDataColumnType<UISession>>;
mockSession.created = 'INVALID';

View file

@ -49,7 +49,8 @@ export const getColumns = (
api: SearchSessionsMgmtAPI,
config: SessionsConfigSchema,
timezone: string,
onActionComplete: OnActionComplete
onActionComplete: OnActionComplete,
kibanaVersion: string
): Array<EuiBasicTableColumn<UISession>> => {
// Use a literal array of table column definitions to detail a UISession object
return [
@ -82,7 +83,7 @@ export const getColumns = (
}),
sortable: true,
width: '20%',
render: (name: UISession['name'], { restoreUrl, reloadUrl, status }) => {
render: (name: UISession['name'], { restoreUrl, reloadUrl, status, version }) => {
const isRestorable = isSessionRestorable(status);
const href = isRestorable ? restoreUrl : reloadUrl;
const trackAction = isRestorable
@ -102,6 +103,21 @@ export const getColumns = (
/>
</>
);
const versionIncompatibleWarning =
isRestorable && version === kibanaVersion ? null : (
<>
{' '}
<EuiIconTip
type="alert"
content={
<FormattedMessage
id="xpack.data.mgmt.searchSessions.table.versionIncompatibleWarning"
defaultMessage="This search session was created in a Kibana instance running a different version. It may not restore correctly."
/>
}
/>
</>
);
return (
<RedirectAppLinks application={core.application}>
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
@ -113,6 +129,7 @@ export const getColumns = (
<TableText>
{name}
{notRestorableWarning}
{versionIncompatibleWarning}
</TableText>
</EuiLink>
</RedirectAppLinks>

View file

@ -40,4 +40,5 @@ export interface UISession {
restoreUrl: string;
initialState: Record<string, unknown>;
restoreState: Record<string, unknown>;
version: string;
}

View file

@ -31,7 +31,12 @@ export class EnhancedDataServerPlugin
public setup(core: CoreSetup<StartDependencies>, deps: SetupDependencies) {
core.savedObjects.registerType(searchSessionSavedObjectType);
this.sessionService = new SearchSessionService(this.logger, this.config, deps.security);
this.sessionService = new SearchSessionService(
this.logger,
this.config,
this.initializerContext.env.packageInfo.version,
deps.security
);
deps.data.__enhance({
search: {

View file

@ -66,6 +66,9 @@ export const searchSessionSavedObjectType: SavedObjectsType = {
username: {
type: 'keyword',
},
version: {
type: 'keyword',
},
},
},
migrations: searchSessionSavedObjectMigrations,

View file

@ -8,58 +8,59 @@
import {
searchSessionSavedObjectMigrations,
SearchSessionSavedObjectAttributesPre$7$13$0,
SearchSessionSavedObjectAttributesPre$7$14$0,
} from './search_session_migration';
import { SavedObject } from '../../../../../src/core/types';
import { SEARCH_SESSION_TYPE, SearchSessionStatus } from '../../../../../src/plugins/data/common';
import { SavedObjectMigrationContext } from 'kibana/server';
const mockCompletedSessionSavedObject: SavedObject<SearchSessionSavedObjectAttributesPre$7$13$0> = {
id: 'id',
type: SEARCH_SESSION_TYPE,
attributes: {
name: 'my_name',
appId: 'my_app_id',
sessionId: 'sessionId',
urlGeneratorId: 'my_url_generator_id',
initialState: {},
restoreState: {},
persisted: true,
idMapping: {},
realmType: 'realmType',
realmName: 'realmName',
username: 'username',
created: '2021-03-26T00:00:00.000Z',
expires: '2021-03-30T00:00:00.000Z',
touched: '2021-03-29T00:00:00.000Z',
status: SearchSessionStatus.COMPLETE,
},
references: [],
};
const mockInProgressSessionSavedObject: SavedObject<SearchSessionSavedObjectAttributesPre$7$13$0> = {
id: 'id',
type: SEARCH_SESSION_TYPE,
attributes: {
name: 'my_name',
appId: 'my_app_id',
sessionId: 'sessionId',
urlGeneratorId: 'my_url_generator_id',
initialState: {},
restoreState: {},
persisted: true,
idMapping: {},
realmType: 'realmType',
realmName: 'realmName',
username: 'username',
created: '2021-03-26T00:00:00.000Z',
expires: '2021-03-30T00:00:00.000Z',
touched: '2021-03-29T00:00:00.000Z',
status: SearchSessionStatus.IN_PROGRESS,
},
references: [],
};
describe('7.12.0 -> 7.13.0', () => {
const mockCompletedSessionSavedObject: SavedObject<SearchSessionSavedObjectAttributesPre$7$13$0> = {
id: 'id',
type: SEARCH_SESSION_TYPE,
attributes: {
name: 'my_name',
appId: 'my_app_id',
sessionId: 'sessionId',
urlGeneratorId: 'my_url_generator_id',
initialState: {},
restoreState: {},
persisted: true,
idMapping: {},
realmType: 'realmType',
realmName: 'realmName',
username: 'username',
created: '2021-03-26T00:00:00.000Z',
expires: '2021-03-30T00:00:00.000Z',
touched: '2021-03-29T00:00:00.000Z',
status: SearchSessionStatus.COMPLETE,
},
references: [],
};
const mockInProgressSessionSavedObject: SavedObject<SearchSessionSavedObjectAttributesPre$7$13$0> = {
id: 'id',
type: SEARCH_SESSION_TYPE,
attributes: {
name: 'my_name',
appId: 'my_app_id',
sessionId: 'sessionId',
urlGeneratorId: 'my_url_generator_id',
initialState: {},
restoreState: {},
persisted: true,
idMapping: {},
realmType: 'realmType',
realmName: 'realmName',
username: 'username',
created: '2021-03-26T00:00:00.000Z',
expires: '2021-03-30T00:00:00.000Z',
touched: '2021-03-29T00:00:00.000Z',
status: SearchSessionStatus.IN_PROGRESS,
},
references: [],
};
const migration = searchSessionSavedObjectMigrations['7.13.0'];
test('"completed" is populated from "touched" for completed session', () => {
const migratedCompletedSession = migration(
@ -106,3 +107,58 @@ describe('7.12.0 -> 7.13.0', () => {
);
});
});
describe('7.13.0 -> 7.14.0', () => {
const mockSessionSavedObject: SavedObject<SearchSessionSavedObjectAttributesPre$7$14$0> = {
id: 'id',
type: SEARCH_SESSION_TYPE,
attributes: {
name: 'my_name',
appId: 'my_app_id',
sessionId: 'sessionId',
urlGeneratorId: 'my_url_generator_id',
initialState: {},
restoreState: {},
persisted: true,
idMapping: {},
realmType: 'realmType',
realmName: 'realmName',
username: 'username',
created: '2021-03-26T00:00:00.000Z',
expires: '2021-03-30T00:00:00.000Z',
touched: '2021-03-29T00:00:00.000Z',
completed: '2021-03-29T00:00:00.000Z',
status: SearchSessionStatus.COMPLETE,
},
references: [],
};
const migration = searchSessionSavedObjectMigrations['7.14.0'];
test('version is populated', () => {
const migratedSession = migration(mockSessionSavedObject, {} as SavedObjectMigrationContext);
expect(migratedSession.attributes).toHaveProperty('version');
expect(migratedSession.attributes.version).toBe('7.13.0');
expect(migratedSession.attributes).toMatchInlineSnapshot(`
Object {
"appId": "my_app_id",
"completed": "2021-03-29T00:00:00.000Z",
"created": "2021-03-26T00:00:00.000Z",
"expires": "2021-03-30T00:00:00.000Z",
"idMapping": Object {},
"initialState": Object {},
"name": "my_name",
"persisted": true,
"realmName": "realmName",
"realmType": "realmType",
"restoreState": Object {},
"sessionId": "sessionId",
"status": "complete",
"touched": "2021-03-29T00:00:00.000Z",
"urlGeneratorId": "my_url_generator_id",
"username": "username",
"version": "7.13.0",
}
`);
});
});

View file

@ -17,14 +17,26 @@ import {
* It is a timestamp representing the session was transitioned into "completed" status.
*/
export type SearchSessionSavedObjectAttributesPre$7$13$0 = Omit<
SearchSessionSavedObjectAttributesLatest,
SearchSessionSavedObjectAttributesPre$7$14$0,
'completed'
>;
/**
* In 7.14.0 a `version` field was added. When search session is created it is populated with current kibana version.
* It is used to display warnings when trying to restore a session from a different version
* For saved object created before 7.14.0 we populate "7.13.0" inside the migration.
* It is less then ideal because the saved object could have actually been created in "7.12.x" or "7.13.x",
* but what is important for 7.14.0 is that the version is less then "7.14.0"
*/
export type SearchSessionSavedObjectAttributesPre$7$14$0 = Omit<
SearchSessionSavedObjectAttributesLatest,
'version'
>;
export const searchSessionSavedObjectMigrations: SavedObjectMigrationMap = {
'7.13.0': (
doc: SavedObjectUnsanitizedDoc<SearchSessionSavedObjectAttributesPre$7$13$0>
): SavedObjectUnsanitizedDoc<SearchSessionSavedObjectAttributesLatest> => {
): SavedObjectUnsanitizedDoc<SearchSessionSavedObjectAttributesPre$7$14$0> => {
if (doc.attributes.status === SearchSessionStatus.COMPLETE) {
return {
...doc,
@ -37,4 +49,15 @@ export const searchSessionSavedObjectMigrations: SavedObjectMigrationMap = {
return doc;
},
'7.14.0': (
doc: SavedObjectUnsanitizedDoc<SearchSessionSavedObjectAttributesPre$7$14$0>
): SavedObjectUnsanitizedDoc<SearchSessionSavedObjectAttributesLatest> => {
return {
...doc,
attributes: {
...doc.attributes,
version: '7.13.0',
},
};
},
};

View file

@ -91,7 +91,7 @@ describe('SearchSessionService', () => {
warn: jest.fn(),
error: jest.fn(),
};
service = new SearchSessionService(mockLogger, config);
service = new SearchSessionService(mockLogger, config, '8.0.0');
const coreStart = coreMock.createStart();
mockTaskManager = taskManagerMock.createStart();
await flushPromises();
@ -171,7 +171,7 @@ describe('SearchSessionService', () => {
warn: jest.fn(),
error: jest.fn(),
};
service = new SearchSessionService(mockLogger, config);
service = new SearchSessionService(mockLogger, config, '8.0.0');
const coreStart = coreMock.createStart();
mockTaskManager = taskManagerMock.createStart();
await flushPromises();

View file

@ -98,6 +98,7 @@ export class SearchSessionService
constructor(
private readonly logger: Logger,
private readonly config: ConfigSchema,
private readonly version: string,
private readonly security?: SecurityPluginSetup
) {
this.sessionConfig = this.config.search.sessions;
@ -330,6 +331,7 @@ export class SearchSessionService
touched: new Date().toISOString(),
idMapping: {},
persisted: false,
version: this.version,
realmType,
realmName,
username,