Skip to content

Commit

Permalink
[Serverless Search] Update the "empty state" screens (#172225)
Browse files Browse the repository at this point in the history
## Summary

This PR offers customizations to the "Getting Started" experience for
users of Serverless Elasticsearch projects. The changes involve checking
Elasticsearch if the currently-logged-in user has created API Keys, and
to use that information to customize the getting started messaging
(destination of the call-to-action of the "no data" prompt) in the UI.

### Screenshots
| | Before | After |
|-|-|-|
| Data Views Mgmt | <img width="1646" alt="before - serverless es data
views mgmt empty state"
src="https://github.com/elastic/kibana/assets/908371/b5c28dda-9ff8-40de-af89-71587bdbbe54">
| <img width="1646" alt="after - serverless es data views mgmt empty
state"
src="https://github.com/elastic/kibana/assets/908371/13aae073-9fef-407a-bdb1-e09d6ea1d168">
|
| Analytics | <img width="1646" alt="before - serverless es discover
empty state"
src="https://github.com/elastic/kibana/assets/908371/ac0e70e5-9a73-4de3-8182-51350099ae87">
| <img width="1646" alt="after - serverless es discover empty state"
src="https://github.com/elastic/kibana/assets/908371/6f9231e0-d2fb-480b-a5a2-79cc9f853a9b">
|

Elasticsearch documentation on API Key listing:
https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-api-key.html

Depends on: #172884
Closes: elastic/search-team#5974
Closes: elastic/search-team#5975

### Other changes
* Created a React hook to send and track a request to the Kibana server
to ask whether the user has access to API Keys
    * Added unit test for this
* See discussion, this hook may be [re-organized
eventually](#172225 (comment))
* Enable lazy loading for the "Analytics No Data" page within analytics
apps.

### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
tsullivan and kibanamachine authored Jan 6, 2024
1 parent ecfa61a commit a12ab58
Show file tree
Hide file tree
Showing 44 changed files with 941 additions and 167 deletions.
2 changes: 1 addition & 1 deletion packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pageLoadAssetSize:
datasetQuality: 50624
dataViewEditor: 28082
dataViewFieldEditor: 27000
dataViewManagement: 5100
dataViewManagement: 5136
dataViews: 48300
dataVisualizer: 27530
devTools: 38637
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const NoDataCard = ({
description: descriptionProp,
canAccessFleet,
button,
href,
...props
}: Props) => {
const styles = NoDataCardStyles();
Expand All @@ -72,7 +73,11 @@ export const NoDataCard = ({
}

// Default footer action is a button with the provided or default string
return <EuiButton fill>{button || titleProp || defaultTitle}</EuiButton>;
return (
<EuiButton fill href={href} data-test-subj="noDataDefaultFooterAction">
{button || titleProp || defaultTitle}
</EuiButton>
);
};

const title = () => {
Expand Down Expand Up @@ -103,6 +108,7 @@ export const NoDataCard = ({
description={description()}
footer={footer()}
isDisabled={!canAccessFleet}
href={href}
image={<Image />}
{...props}
/>
Expand Down
2 changes: 1 addition & 1 deletion packages/shared-ux/card/no_data/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export type NoDataCardKibanaDependencies = KibanaDependencies & RedirectAppLinks
* Props for the `NoDataCard` pure component.
*/
export type NoDataCardComponentProps = Partial<
Omit<EuiCardProps, 'layout' | 'isDisabled' | 'button' | 'onClick' | 'description'>
Pick<EuiCardProps, 'className' | 'href' | 'title'>
> & {
/**
* Provide just a string for the button's label, or a whole component;
Expand Down
2 changes: 2 additions & 0 deletions packages/shared-ux/page/analytics_no_data/impl/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ export type {
AnalyticsNoDataPageKibanaDependencies,
AnalyticsNoDataPageProps,
} from '@kbn/shared-ux-page-analytics-no-data-types';

export { getHasApiKeys$ } from './lib/get_has_api_keys';
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { HttpSetup } from '@kbn/core-http-browser';
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
import { HasApiKeysResponse, getHasApiKeys$ } from './get_has_api_keys';

describe('getHasApiKeys$', () => {
let mockHttp: HttpSetup;
beforeEach(() => {
mockHttp = httpServiceMock.createSetupContract({ basePath: '/test' });
});

it('should return the correct sequence of states', (done) => {
const httpGetSpy = jest.spyOn(mockHttp, 'get');
httpGetSpy.mockResolvedValue({ hasApiKeys: true });
const source$ = getHasApiKeys$(mockHttp);

const emittedValues: HasApiKeysResponse[] = [];

source$.subscribe({
next: (value) => emittedValues.push(value),
complete: () => {
expect(emittedValues).toEqual([
{ error: null, hasApiKeys: null, isLoading: true },
{ error: null, hasApiKeys: true, isLoading: false },
]);
done();
},
});
});

it('should forward the error', (done) => {
const httpGetSpy = jest.spyOn(mockHttp, 'get');
httpGetSpy.mockRejectedValue('something bad');
const source$ = getHasApiKeys$(mockHttp);

const emittedValues: HasApiKeysResponse[] = [];

source$.subscribe({
next: (value) => emittedValues.push(value),
complete: () => {
expect(emittedValues).toEqual([
{ error: null, hasApiKeys: null, isLoading: true },
{ error: 'something bad', hasApiKeys: null, isLoading: false },
]);
done();
},
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import type { AnalyticsNoDataPageServices } from '@kbn/shared-ux-page-analytics-no-data-types';
import { of, Observable, catchError, from, map, startWith } from 'rxjs';

export interface HasApiKeysEndpointResponseData {
hasApiKeys: boolean;
}

export interface HasApiKeysResponse {
hasApiKeys: boolean | null;
isLoading: boolean;
error: Error | null;
}

const HAS_API_KEYS_ENDPOINT_PATH = '/internal/security/api_key/_has_active';

export const getHasApiKeys$ = ({
get,
}: {
get: AnalyticsNoDataPageServices['getHttp'];
}): Observable<HasApiKeysResponse> => {
return from(get<HasApiKeysEndpointResponseData>(HAS_API_KEYS_ENDPOINT_PATH)).pipe(
map((responseData) => {
return {
isLoading: false,
hasApiKeys: responseData.hasApiKeys,
error: null,
};
}),
startWith({
isLoading: true,
hasApiKeys: null,
error: null,
}),
// catch any errors
catchError((error) => {
// eslint-disable-next-line no-console
console.error('Could not determine whether user has API keys:', error);

return of({
hasApiKeys: null,
isLoading: false,
error,
});
})
);
};
Loading

0 comments on commit a12ab58

Please sign in to comment.