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:
Anton Dosov 2024-12-30 16:35:51 +01:00 committed by GitHub
parent 8cc2f2b9c8
commit 70cf414f42
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 150 additions and 54 deletions

View file

@ -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`
);

View file

@ -23,5 +23,6 @@
"@kbn/content-management-favorites-server",
"@kbn/i18n-react",
"@kbn/usage-collection-plugin",
"@kbn/core-user-profile-browser",
]
}

View file

@ -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,

View file

@ -75,7 +75,7 @@ export const getStoryServices = (params: Params, action: ActionFn = () => {}) =>
getTagManagementUrl: () => '',
getTagIdsFromReferences: () => [],
isTaggingEnabled: () => true,
isFavoritesEnabled: () => false,
isFavoritesEnabled: () => Promise.resolve(false),
isKibanaVersioningEnabled: false,
...params,
};

View file

@ -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}

View file

@ -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 */}

View file

@ -33,6 +33,7 @@ exports[`renders matching snapshot 1`] = `
Object {
"bulkGet": [MockFunction],
"getCurrent": [MockFunction],
"getEnabled$": [MockFunction],
"getUserProfile$": [MockFunction],
"partialUpdate": [MockFunction],
"suggest": [MockFunction],

View file

@ -35,6 +35,7 @@ Array [
Object {
"bulkGet": [MockFunction],
"getCurrent": [MockFunction],
"getEnabled$": [MockFunction],
"getUserProfile$": [MockFunction],
"partialUpdate": [MockFunction],
"suggest": [MockFunction],

View file

@ -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],

View file

@ -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],

View file

@ -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();

View file

@ -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),

View file

@ -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([]),

View file

@ -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(),

View file

@ -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>;
};

View file

@ -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.

View file

@ -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',

View file

@ -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[] = [];

View file

@ -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();
});
});
});
});

View file

@ -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,

View file

@ -54,6 +54,7 @@ export const DashboardListing = ({
return new FavoritesClient(DASHBOARD_APP_ID, DASHBOARD_CONTENT_ID, {
http: coreServices.http,
usageCollection: usageCollectionService,
userProfile: coreServices.userProfile,
});
}, []);

View file

@ -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>;
};

View file

@ -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",
]