mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Gracefully disable favorites if profile is not available (#204397)
## Summary When a user profile is not available, the favorites (starred) service can't be used. On UI user profile can be not available if security is disabled or for an anonymous user. This PR improves the handling of starred features for rare cases when a profile is missing: - No unnecessary `GET favorites` requests that would fail with error and add noise to console/networks - No unhandled errors are thrown - Starred tab in esql is hidden - The Dashboard Starred tab isn't flickering on each attempt to fetch favorites For this needed to expose `userProfile.enabled$` from core, also created https://github.com/elastic/kibana/issues/204570 ### Testing ``` node scripts/functional_tests_server.js --config test/functional/apps/dashboard/group4/config.ts localhost:5620 ``` another way is by configuring an anonymous user https://www.elastic.co/guide/en/elasticsearch/reference/current/anonymous-access.html
This commit is contained in:
parent
8cc2f2b9c8
commit
70cf414f42
23 changed files with 150 additions and 54 deletions
|
@ -9,11 +9,13 @@
|
|||
|
||||
import type { HttpStart } from '@kbn/core-http-browser';
|
||||
import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
|
||||
import type { UserProfileServiceStart } from '@kbn/core-user-profile-browser';
|
||||
import type {
|
||||
GetFavoritesResponse as GetFavoritesResponseServer,
|
||||
AddFavoriteResponse,
|
||||
GetFavoritesResponse as GetFavoritesResponseServer,
|
||||
RemoveFavoriteResponse,
|
||||
} from '@kbn/content-management-favorites-server';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
export interface GetFavoritesResponse<Metadata extends object | void = void>
|
||||
extends GetFavoritesResponseServer {
|
||||
|
@ -29,6 +31,7 @@ export interface FavoritesClientPublic<Metadata extends object | void = void> {
|
|||
addFavorite(params: AddFavoriteRequest<Metadata>): Promise<AddFavoriteResponse>;
|
||||
removeFavorite(params: { id: string }): Promise<RemoveFavoriteResponse>;
|
||||
|
||||
isAvailable(): Promise<boolean>;
|
||||
getFavoriteType(): string;
|
||||
reportAddFavoriteClick(): void;
|
||||
reportRemoveFavoriteClick(): void;
|
||||
|
@ -40,14 +43,29 @@ export class FavoritesClient<Metadata extends object | void = void>
|
|||
constructor(
|
||||
private readonly appName: string,
|
||||
private readonly favoriteObjectType: string,
|
||||
private readonly deps: { http: HttpStart; usageCollection?: UsageCollectionStart }
|
||||
private readonly deps: {
|
||||
http: HttpStart;
|
||||
userProfile: UserProfileServiceStart;
|
||||
usageCollection?: UsageCollectionStart;
|
||||
}
|
||||
) {}
|
||||
|
||||
public async isAvailable(): Promise<boolean> {
|
||||
return firstValueFrom(this.deps.userProfile.getEnabled$());
|
||||
}
|
||||
|
||||
private async ifAvailablePreCheck() {
|
||||
if (!(await this.isAvailable()))
|
||||
throw new Error('Favorites service is not available for current user');
|
||||
}
|
||||
|
||||
public async getFavorites(): Promise<GetFavoritesResponse<Metadata>> {
|
||||
await this.ifAvailablePreCheck();
|
||||
return this.deps.http.get(`/internal/content_management/favorites/${this.favoriteObjectType}`);
|
||||
}
|
||||
|
||||
public async addFavorite(params: AddFavoriteRequest<Metadata>): Promise<AddFavoriteResponse> {
|
||||
await this.ifAvailablePreCheck();
|
||||
return this.deps.http.post(
|
||||
`/internal/content_management/favorites/${this.favoriteObjectType}/${params.id}/favorite`,
|
||||
{ body: 'metadata' in params ? JSON.stringify({ metadata: params.metadata }) : undefined }
|
||||
|
@ -55,6 +73,7 @@ export class FavoritesClient<Metadata extends object | void = void>
|
|||
}
|
||||
|
||||
public async removeFavorite({ id }: { id: string }): Promise<RemoveFavoriteResponse> {
|
||||
await this.ifAvailablePreCheck();
|
||||
return this.deps.http.post(
|
||||
`/internal/content_management/favorites/${this.favoriteObjectType}/${id}/unfavorite`
|
||||
);
|
||||
|
|
|
@ -23,5 +23,6 @@
|
|||
"@kbn/content-management-favorites-server",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/usage-collection-plugin",
|
||||
"@kbn/core-user-profile-browser",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ export const getMockServices = (overrides?: Partial<Services & UserProfilesServi
|
|||
getTagManagementUrl: () => '',
|
||||
getTagIdsFromReferences: () => [],
|
||||
isTaggingEnabled: () => true,
|
||||
isFavoritesEnabled: () => false,
|
||||
isFavoritesEnabled: () => Promise.resolve(false),
|
||||
bulkGetUserProfiles: async () => [],
|
||||
getUserProfile: async () => ({ uid: '', enabled: true, data: {}, user: { username: '' } }),
|
||||
isKibanaVersioningEnabled: false,
|
||||
|
|
|
@ -75,7 +75,7 @@ export const getStoryServices = (params: Params, action: ActionFn = () => {}) =>
|
|||
getTagManagementUrl: () => '',
|
||||
getTagIdsFromReferences: () => [],
|
||||
isTaggingEnabled: () => true,
|
||||
isFavoritesEnabled: () => false,
|
||||
isFavoritesEnabled: () => Promise.resolve(false),
|
||||
isKibanaVersioningEnabled: false,
|
||||
...params,
|
||||
};
|
||||
|
|
|
@ -73,7 +73,7 @@ export interface Services {
|
|||
/** Predicate to indicate if tagging features is enabled */
|
||||
isTaggingEnabled: () => boolean;
|
||||
/** Predicate to indicate if favorites features is enabled */
|
||||
isFavoritesEnabled: () => boolean;
|
||||
isFavoritesEnabled: () => Promise<boolean>;
|
||||
/** Predicate function to indicate if some of the saved object references are tags */
|
||||
itemHasTags: (references: SavedObjectsReference[]) => boolean;
|
||||
/** Handler to return the url to navigate to the kibana tags management */
|
||||
|
@ -288,7 +288,7 @@ export const TableListViewKibanaProvider: FC<
|
|||
currentAppId$={application.currentAppId$}
|
||||
navigateToUrl={application.navigateToUrl}
|
||||
isTaggingEnabled={() => Boolean(savedObjectsTagging)}
|
||||
isFavoritesEnabled={() => Boolean(services.favorites)}
|
||||
isFavoritesEnabled={async () => services.favorites?.isAvailable() ?? false}
|
||||
getTagList={getTagList}
|
||||
TagList={TagList}
|
||||
itemHasTags={itemHasTags}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
import React, { useReducer, useCallback, useEffect, useRef, useMemo } from 'react';
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import {
|
||||
EuiBasicTableColumn,
|
||||
EuiButton,
|
||||
|
@ -379,6 +380,8 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
|
|||
isKibanaVersioningEnabled,
|
||||
} = useServices();
|
||||
|
||||
const favoritesEnabled = useAsync(isFavoritesEnabled, [])?.value ?? false;
|
||||
|
||||
const openContentEditor = useOpenContentEditor();
|
||||
const contentInsightsServices = useContentInsightsServices();
|
||||
|
||||
|
@ -621,7 +624,7 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
|
|||
}
|
||||
}}
|
||||
searchTerm={searchQuery.text}
|
||||
isFavoritesEnabled={isFavoritesEnabled()}
|
||||
isFavoritesEnabled={favoritesEnabled}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
@ -754,7 +757,7 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
|
|||
tableItemsRowActions,
|
||||
inspectItem,
|
||||
entityName,
|
||||
isFavoritesEnabled,
|
||||
favoritesEnabled,
|
||||
isKibanaVersioningEnabled,
|
||||
]);
|
||||
|
||||
|
@ -1218,7 +1221,7 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
|
|||
addOrRemoveExcludeTagFilter={addOrRemoveExcludeTagFilter}
|
||||
clearTagSelection={clearTagSelection}
|
||||
createdByEnabled={createdByEnabled}
|
||||
favoritesEnabled={isFavoritesEnabled()}
|
||||
favoritesEnabled={favoritesEnabled}
|
||||
/>
|
||||
|
||||
{/* Delete modal */}
|
||||
|
|
|
@ -33,6 +33,7 @@ exports[`renders matching snapshot 1`] = `
|
|||
Object {
|
||||
"bulkGet": [MockFunction],
|
||||
"getCurrent": [MockFunction],
|
||||
"getEnabled$": [MockFunction],
|
||||
"getUserProfile$": [MockFunction],
|
||||
"partialUpdate": [MockFunction],
|
||||
"suggest": [MockFunction],
|
||||
|
|
|
@ -35,6 +35,7 @@ Array [
|
|||
Object {
|
||||
"bulkGet": [MockFunction],
|
||||
"getCurrent": [MockFunction],
|
||||
"getEnabled$": [MockFunction],
|
||||
"getUserProfile$": [MockFunction],
|
||||
"partialUpdate": [MockFunction],
|
||||
"suggest": [MockFunction],
|
||||
|
|
|
@ -43,6 +43,7 @@ Array [
|
|||
Object {
|
||||
"bulkGet": [MockFunction],
|
||||
"getCurrent": [MockFunction],
|
||||
"getEnabled$": [MockFunction],
|
||||
"getUserProfile$": [MockFunction],
|
||||
"partialUpdate": [MockFunction],
|
||||
"suggest": [MockFunction],
|
||||
|
@ -183,6 +184,7 @@ Array [
|
|||
Object {
|
||||
"bulkGet": [MockFunction],
|
||||
"getCurrent": [MockFunction],
|
||||
"getEnabled$": [MockFunction],
|
||||
"getUserProfile$": [MockFunction],
|
||||
"partialUpdate": [MockFunction],
|
||||
"suggest": [MockFunction],
|
||||
|
@ -316,6 +318,7 @@ Array [
|
|||
Object {
|
||||
"bulkGet": [MockFunction],
|
||||
"getCurrent": [MockFunction],
|
||||
"getEnabled$": [MockFunction],
|
||||
"getUserProfile$": [MockFunction],
|
||||
"partialUpdate": [MockFunction],
|
||||
"suggest": [MockFunction],
|
||||
|
|
|
@ -125,6 +125,7 @@ Array [
|
|||
Object {
|
||||
"bulkGet": [MockFunction],
|
||||
"getCurrent": [MockFunction],
|
||||
"getEnabled$": [MockFunction],
|
||||
"getUserProfile$": [MockFunction],
|
||||
"partialUpdate": [MockFunction],
|
||||
"suggest": [MockFunction],
|
||||
|
@ -424,6 +425,7 @@ Array [
|
|||
Object {
|
||||
"bulkGet": [MockFunction],
|
||||
"getCurrent": [MockFunction],
|
||||
"getEnabled$": [MockFunction],
|
||||
"getUserProfile$": [MockFunction],
|
||||
"partialUpdate": [MockFunction],
|
||||
"suggest": [MockFunction],
|
||||
|
@ -788,6 +790,7 @@ Array [
|
|||
Object {
|
||||
"bulkGet": [MockFunction],
|
||||
"getCurrent": [MockFunction],
|
||||
"getEnabled$": [MockFunction],
|
||||
"getUserProfile$": [MockFunction],
|
||||
"partialUpdate": [MockFunction],
|
||||
"suggest": [MockFunction],
|
||||
|
@ -1071,6 +1074,7 @@ Array [
|
|||
Object {
|
||||
"bulkGet": [MockFunction],
|
||||
"getCurrent": [MockFunction],
|
||||
"getEnabled$": [MockFunction],
|
||||
"getUserProfile$": [MockFunction],
|
||||
"partialUpdate": [MockFunction],
|
||||
"suggest": [MockFunction],
|
||||
|
@ -1359,6 +1363,7 @@ Array [
|
|||
Object {
|
||||
"bulkGet": [MockFunction],
|
||||
"getCurrent": [MockFunction],
|
||||
"getEnabled$": [MockFunction],
|
||||
"getUserProfile$": [MockFunction],
|
||||
"partialUpdate": [MockFunction],
|
||||
"suggest": [MockFunction],
|
||||
|
@ -1642,6 +1647,7 @@ Array [
|
|||
Object {
|
||||
"bulkGet": [MockFunction],
|
||||
"getCurrent": [MockFunction],
|
||||
"getEnabled$": [MockFunction],
|
||||
"getUserProfile$": [MockFunction],
|
||||
"partialUpdate": [MockFunction],
|
||||
"suggest": [MockFunction],
|
||||
|
@ -1698,6 +1704,7 @@ Array [
|
|||
Object {
|
||||
"bulkGet": [MockFunction],
|
||||
"getCurrent": [MockFunction],
|
||||
"getEnabled$": [MockFunction],
|
||||
"getUserProfile$": [MockFunction],
|
||||
"partialUpdate": [MockFunction],
|
||||
"suggest": [MockFunction],
|
||||
|
@ -1869,6 +1876,7 @@ Array [
|
|||
Object {
|
||||
"bulkGet": [MockFunction],
|
||||
"getCurrent": [MockFunction],
|
||||
"getEnabled$": [MockFunction],
|
||||
"getUserProfile$": [MockFunction],
|
||||
"partialUpdate": [MockFunction],
|
||||
"suggest": [MockFunction],
|
||||
|
@ -2002,6 +2010,7 @@ Array [
|
|||
Object {
|
||||
"bulkGet": [MockFunction],
|
||||
"getCurrent": [MockFunction],
|
||||
"getEnabled$": [MockFunction],
|
||||
"getUserProfile$": [MockFunction],
|
||||
"partialUpdate": [MockFunction],
|
||||
"suggest": [MockFunction],
|
||||
|
@ -2140,6 +2149,7 @@ Array [
|
|||
Object {
|
||||
"bulkGet": [MockFunction],
|
||||
"getCurrent": [MockFunction],
|
||||
"getEnabled$": [MockFunction],
|
||||
"getUserProfile$": [MockFunction],
|
||||
"partialUpdate": [MockFunction],
|
||||
"suggest": [MockFunction],
|
||||
|
@ -2273,6 +2283,7 @@ Array [
|
|||
Object {
|
||||
"bulkGet": [MockFunction],
|
||||
"getCurrent": [MockFunction],
|
||||
"getEnabled$": [MockFunction],
|
||||
"getUserProfile$": [MockFunction],
|
||||
"partialUpdate": [MockFunction],
|
||||
"suggest": [MockFunction],
|
||||
|
|
|
@ -19,6 +19,7 @@ describe('convertUserProfileAPI', () => {
|
|||
beforeEach(() => {
|
||||
source = {
|
||||
userProfile$: of(null),
|
||||
enabled$: of(false),
|
||||
getCurrent: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
suggest: jest.fn(),
|
||||
|
@ -34,6 +35,12 @@ describe('convertUserProfileAPI', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getEnabled$', () => {
|
||||
it('returns the observable from the source', () => {
|
||||
expect(output.getEnabled$()).toBe(source.enabled$);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCurrent', () => {
|
||||
it('calls the API from the source with the correct parameters', () => {
|
||||
output.getCurrent();
|
||||
|
|
|
@ -15,6 +15,7 @@ export const convertUserProfileAPI = (
|
|||
): InternalUserProfileServiceStart => {
|
||||
return {
|
||||
getUserProfile$: () => delegate.userProfile$,
|
||||
getEnabled$: () => delegate.enabled$,
|
||||
getCurrent: delegate.getCurrent.bind(delegate),
|
||||
bulkGet: delegate.bulkGet.bind(delegate),
|
||||
suggest: delegate.suggest.bind(delegate),
|
||||
|
|
|
@ -17,6 +17,7 @@ import { UserProfileData } from '@kbn/core-user-profile-common';
|
|||
export const getDefaultUserProfileImplementation = (): CoreUserProfileDelegateContract => {
|
||||
return {
|
||||
userProfile$: of(null),
|
||||
enabled$: of(false),
|
||||
getCurrent: <D extends UserProfileData>() =>
|
||||
Promise.resolve(null as unknown as GetUserProfileResponse<D>),
|
||||
bulkGet: () => Promise.resolve([]),
|
||||
|
|
|
@ -28,6 +28,7 @@ const createSetupMock = () => {
|
|||
const createStartMock = () => {
|
||||
const mock: jest.Mocked<UserProfileServiceStart> = {
|
||||
getUserProfile$: jest.fn().mockReturnValue(of(null)),
|
||||
getEnabled$: jest.fn().mockReturnValue(of(false)),
|
||||
getCurrent: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
suggest: jest.fn(),
|
||||
|
@ -49,6 +50,7 @@ const createInternalSetupMock = () => {
|
|||
const createInternalStartMock = () => {
|
||||
const mock: jest.Mocked<InternalUserProfileServiceStart> = {
|
||||
getUserProfile$: jest.fn().mockReturnValue(of(null)),
|
||||
getEnabled$: jest.fn().mockReturnValue(of(false)),
|
||||
getCurrent: jest.fn(),
|
||||
bulkGet: jest.fn(),
|
||||
suggest: jest.fn(),
|
||||
|
|
|
@ -11,6 +11,10 @@ import type { Observable } from 'rxjs';
|
|||
import type { UserProfileData } from '@kbn/core-user-profile-common';
|
||||
import type { UserProfileService } from './service';
|
||||
|
||||
export type CoreUserProfileDelegateContract = Omit<UserProfileService, 'getUserProfile$'> & {
|
||||
export type CoreUserProfileDelegateContract = Omit<
|
||||
UserProfileService,
|
||||
'getUserProfile$' | 'getEnabled$'
|
||||
> & {
|
||||
userProfile$: Observable<UserProfileData | null>;
|
||||
enabled$: Observable<boolean>;
|
||||
};
|
||||
|
|
|
@ -17,10 +17,13 @@ import type {
|
|||
|
||||
export interface UserProfileService {
|
||||
/**
|
||||
* Retrieve an observable emitting when the user profile is loaded.
|
||||
* Retrieve an observable emitting the current user profile data.
|
||||
*/
|
||||
getUserProfile$(): Observable<UserProfileData | null>;
|
||||
|
||||
/** Flag to indicate if the current user has a user profile. Anonymous users don't have user profiles. */
|
||||
getEnabled$(): Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Retrieves the user profile of the current user. If the profile isn't available, e.g. for the anonymous users or
|
||||
* users authenticated via authenticating proxies, the `null` value is returned.
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
import { EsqlStarredQueriesService } from './esql_starred_queries_service';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import type { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
class LocalStorageMock {
|
||||
public store: Record<string, unknown>;
|
||||
|
@ -34,20 +35,36 @@ describe('EsqlStarredQueriesService', () => {
|
|||
const core = coreMock.createStart();
|
||||
const storage = new LocalStorageMock({}) as unknown as Storage;
|
||||
|
||||
it('should initialize', async () => {
|
||||
const isUserProfileEnabled$ = new BehaviorSubject<boolean>(true);
|
||||
jest.spyOn(core.userProfile, 'getEnabled$').mockImplementation(() => isUserProfileEnabled$);
|
||||
|
||||
beforeEach(() => {
|
||||
isUserProfileEnabled$.next(true);
|
||||
});
|
||||
|
||||
const initialize = async () => {
|
||||
const service = await EsqlStarredQueriesService.initialize({
|
||||
http: core.http,
|
||||
userProfile: core.userProfile,
|
||||
storage,
|
||||
});
|
||||
return service!;
|
||||
};
|
||||
|
||||
it('should return null if favorites service not available', async () => {
|
||||
isUserProfileEnabled$.next(false);
|
||||
const service = await initialize();
|
||||
expect(service).toBeNull();
|
||||
});
|
||||
|
||||
it('should initialize', async () => {
|
||||
const service = await initialize();
|
||||
expect(service).toBeDefined();
|
||||
expect(service.queries$.value).toEqual([]);
|
||||
});
|
||||
|
||||
it('should add a new starred query', async () => {
|
||||
const service = await EsqlStarredQueriesService.initialize({
|
||||
http: core.http,
|
||||
storage,
|
||||
});
|
||||
const service = await initialize();
|
||||
const query = {
|
||||
queryString: 'SELECT * FROM test',
|
||||
timeRan: '2021-09-01T00:00:00Z',
|
||||
|
@ -66,10 +83,7 @@ describe('EsqlStarredQueriesService', () => {
|
|||
});
|
||||
|
||||
it('should not add the same query twice', async () => {
|
||||
const service = await EsqlStarredQueriesService.initialize({
|
||||
http: core.http,
|
||||
storage,
|
||||
});
|
||||
const service = await initialize();
|
||||
const query = {
|
||||
queryString: 'SELECT * FROM test',
|
||||
timeRan: '2021-09-01T00:00:00Z',
|
||||
|
@ -94,10 +108,7 @@ describe('EsqlStarredQueriesService', () => {
|
|||
});
|
||||
|
||||
it('should add the query trimmed', async () => {
|
||||
const service = await EsqlStarredQueriesService.initialize({
|
||||
http: core.http,
|
||||
storage,
|
||||
});
|
||||
const service = await initialize();
|
||||
const query = {
|
||||
queryString: `SELECT * FROM test |
|
||||
WHERE field != 'value'`,
|
||||
|
@ -118,10 +129,7 @@ describe('EsqlStarredQueriesService', () => {
|
|||
});
|
||||
|
||||
it('should remove a query', async () => {
|
||||
const service = await EsqlStarredQueriesService.initialize({
|
||||
http: core.http,
|
||||
storage,
|
||||
});
|
||||
const service = await initialize();
|
||||
const query = {
|
||||
queryString: `SELECT * FROM test | WHERE field != 'value'`,
|
||||
timeRan: '2021-09-01T00:00:00Z',
|
||||
|
@ -144,10 +152,7 @@ describe('EsqlStarredQueriesService', () => {
|
|||
});
|
||||
|
||||
it('should return the button correctly', async () => {
|
||||
const service = await EsqlStarredQueriesService.initialize({
|
||||
http: core.http,
|
||||
storage,
|
||||
});
|
||||
const service = await initialize();
|
||||
const query = {
|
||||
queryString: 'SELECT * FROM test',
|
||||
timeRan: '2021-09-01T00:00:00Z',
|
||||
|
@ -162,10 +167,7 @@ describe('EsqlStarredQueriesService', () => {
|
|||
});
|
||||
|
||||
it('should display the modal when the Remove button is clicked', async () => {
|
||||
const service = await EsqlStarredQueriesService.initialize({
|
||||
http: core.http,
|
||||
storage,
|
||||
});
|
||||
const service = await initialize();
|
||||
const query = {
|
||||
queryString: 'SELECT * FROM test',
|
||||
timeRan: '2021-09-01T00:00:00Z',
|
||||
|
@ -183,10 +185,7 @@ describe('EsqlStarredQueriesService', () => {
|
|||
|
||||
it('should NOT display the modal when Remove the button is clicked but the user has dismissed the modal permanently', async () => {
|
||||
storage.set('esqlEditor.starredQueriesDiscard', true);
|
||||
const service = await EsqlStarredQueriesService.initialize({
|
||||
http: core.http,
|
||||
storage,
|
||||
});
|
||||
const service = await initialize();
|
||||
const query = {
|
||||
queryString: 'SELECT * FROM test',
|
||||
timeRan: '2021-09-01T00:00:00Z',
|
||||
|
|
|
@ -43,6 +43,7 @@ export interface StarredQueryItem extends QueryHistoryItem {
|
|||
|
||||
interface EsqlStarredQueriesServices {
|
||||
http: CoreStart['http'];
|
||||
userProfile: CoreStart['userProfile'];
|
||||
storage: Storage;
|
||||
usageCollection?: UsageCollectionStart;
|
||||
}
|
||||
|
@ -81,9 +82,13 @@ export class EsqlStarredQueriesService {
|
|||
static async initialize(services: EsqlStarredQueriesServices) {
|
||||
const client = new FavoritesClient<StarredQueryMetadata>('esql_editor', 'esql_query', {
|
||||
http: services.http,
|
||||
userProfile: services.userProfile,
|
||||
usageCollection: services.usageCollection,
|
||||
});
|
||||
|
||||
const isAvailable = await client.isAvailable();
|
||||
if (!isAvailable) return null;
|
||||
|
||||
const { favoriteMetadata } = (await client?.getFavorites()) || {};
|
||||
const retrievedQueries: StarredQueryItem[] = [];
|
||||
|
||||
|
|
|
@ -10,13 +10,14 @@
|
|||
import React from 'react';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
QueryHistoryAction,
|
||||
getTableColumns,
|
||||
QueryColumn,
|
||||
HistoryAndStarredQueriesTabs,
|
||||
} from './history_starred_queries';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
jest.mock('../history_local_storage', () => {
|
||||
const module = jest.requireActual('../history_local_storage');
|
||||
|
@ -218,6 +219,7 @@ describe('Starred and History queries components', () => {
|
|||
const services = {
|
||||
core: coreMock.createStart(),
|
||||
};
|
||||
|
||||
it('should render two tabs', () => {
|
||||
render(
|
||||
<KibanaContextProvider services={services}>
|
||||
|
@ -271,5 +273,30 @@ describe('Starred and History queries components', () => {
|
|||
'Showing 0 queries (max 100)'
|
||||
);
|
||||
});
|
||||
|
||||
it('should hide starred tab if starred service failed to initialize', async () => {
|
||||
jest.spyOn(services.core.userProfile, 'getEnabled$').mockImplementation(() => of(false));
|
||||
|
||||
render(
|
||||
<KibanaContextProvider services={services}>
|
||||
<HistoryAndStarredQueriesTabs
|
||||
containerCSS={{}}
|
||||
containerWidth={1024}
|
||||
onUpdateAndSubmit={jest.fn()}
|
||||
height={200}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
||||
// initial render two tabs are shown
|
||||
expect(screen.getByTestId('history-queries-tab')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('history-queries-tab')).toHaveTextContent('Recent');
|
||||
expect(screen.getByTestId('starred-queries-tab')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('starred-queries-tab')).toHaveTextContent('Starred');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('starred-queries-tab')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -470,22 +470,30 @@ export function HistoryAndStarredQueriesTabs({
|
|||
const kibana = useKibana<ESQLEditorDeps>();
|
||||
const { core, usageCollection, storage } = kibana.services;
|
||||
|
||||
const [starredQueriesService, setStarredQueriesService] = useState<EsqlStarredQueriesService>();
|
||||
const [starredQueriesService, setStarredQueriesService] = useState<
|
||||
EsqlStarredQueriesService | null | undefined
|
||||
>();
|
||||
const [starredQueries, setStarredQueries] = useState<StarredQueryItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const initializeService = async () => {
|
||||
const starredService = await EsqlStarredQueriesService.initialize({
|
||||
http: core.http,
|
||||
userProfile: core.userProfile,
|
||||
usageCollection,
|
||||
storage,
|
||||
});
|
||||
setStarredQueriesService(starredService);
|
||||
|
||||
if (starredService) {
|
||||
setStarredQueriesService(starredService);
|
||||
} else {
|
||||
setStarredQueriesService(null);
|
||||
}
|
||||
};
|
||||
if (!starredQueriesService) {
|
||||
initializeService();
|
||||
}
|
||||
}, [core.http, starredQueriesService, storage, usageCollection]);
|
||||
}, [core.http, core.userProfile, starredQueriesService, storage, usageCollection]);
|
||||
|
||||
starredQueriesService?.queries$.subscribe((nextQueries) => {
|
||||
if (nextQueries.length !== starredQueries.length) {
|
||||
|
@ -495,7 +503,11 @@ export function HistoryAndStarredQueriesTabs({
|
|||
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const tabs = useMemo(() => {
|
||||
return [
|
||||
// use typed helper instead of .filter directly to remove falsy values from result type
|
||||
function filterMissing<T>(array: Array<T | false>): T[] {
|
||||
return array.filter((item): item is T => item !== undefined);
|
||||
}
|
||||
return filterMissing([
|
||||
{
|
||||
id: 'history-queries-tab',
|
||||
name: i18n.translate('esqlEditor.query.historyQueriesTabLabel', {
|
||||
|
@ -513,11 +525,11 @@ export function HistoryAndStarredQueriesTabs({
|
|||
tableCaption={i18n.translate('esqlEditor.query.querieshistoryTable', {
|
||||
defaultMessage: 'Queries history table',
|
||||
})}
|
||||
starredQueriesService={starredQueriesService}
|
||||
starredQueriesService={starredQueriesService ?? undefined}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
starredQueriesService !== null && {
|
||||
id: 'starred-queries-tab',
|
||||
dataTestSubj: 'starred-queries-tab',
|
||||
name: i18n.translate('esqlEditor.query.starredQueriesTabLabel', {
|
||||
|
@ -539,12 +551,12 @@ export function HistoryAndStarredQueriesTabs({
|
|||
tableCaption={i18n.translate('esqlEditor.query.starredQueriesTable', {
|
||||
defaultMessage: 'Starred queries table',
|
||||
})}
|
||||
starredQueriesService={starredQueriesService}
|
||||
starredQueriesService={starredQueriesService ?? undefined}
|
||||
isStarredTab={true}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
]);
|
||||
}, [
|
||||
containerCSS,
|
||||
containerWidth,
|
||||
|
|
|
@ -54,6 +54,7 @@ export const DashboardListing = ({
|
|||
return new FavoritesClient(DASHBOARD_APP_ID, DASHBOARD_CONTENT_ID, {
|
||||
http: coreServices.http,
|
||||
usageCollection: usageCollectionService,
|
||||
userProfile: coreServices.userProfile,
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import type { Observable } from 'rxjs';
|
||||
|
||||
import type { CoreUserProfileDelegateContract } from '@kbn/core-user-profile-browser';
|
||||
import type { UserProfileData } from '@kbn/core-user-profile-common';
|
||||
|
||||
export type {
|
||||
GetUserProfileResponse,
|
||||
|
@ -18,13 +17,10 @@ export type {
|
|||
} from '@kbn/core-user-profile-browser';
|
||||
|
||||
export type UserProfileAPIClient = CoreUserProfileDelegateContract & {
|
||||
readonly userProfile$: Observable<UserProfileData | null>;
|
||||
/**
|
||||
* Indicates if the user profile data has been loaded from the server.
|
||||
* Useful to distinguish between the case when the user profile data is `null` because the HTTP
|
||||
* request has not finished or because there is no user profile data for the current user.
|
||||
*/
|
||||
readonly userProfileLoaded$: Observable<boolean>;
|
||||
/** Flag to indicate if the current user has a user profile. Anonymous users don't have user profiles. */
|
||||
readonly enabled$: Observable<boolean>;
|
||||
};
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core-user-profile-browser",
|
||||
"@kbn/core-user-profile-common",
|
||||
"@kbn/security-plugin-types-common",
|
||||
"@kbn/core-security-common",
|
||||
]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue