Skip to content

Commit c6959da

Browse files
Merge branch 'master' into canvas-app-navigation
2 parents d9a4408 + 84993c6 commit c6959da

168 files changed

Lines changed: 2575 additions & 1985 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CONTRIBUTING.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,14 @@ module.exports = {
490490
};
491491
```
492492

493+
APM [Real User Monitoring agent](https://www.elastic.co/guide/en/apm/agent/rum-js/current/index.html) is not available in the Kibana distributables,
494+
however the agent can be enabled by setting `ELASTIC_APM_ACTIVE` to `true`.
495+
flags
496+
```
497+
ELASTIC_APM_ACTIVE=true yarn start
498+
// activates both Node.js and RUM agent
499+
```
500+
493501
Once the agent is active, it will trace all incoming HTTP requests to Kibana, monitor for errors, and collect process-level metrics.
494502
The collected data will be sent to the APM Server and is viewable in the APM UI in Kibana.
495503

docs/development/core/public/kibana-plugin-core-public.applicationstart.geturlforapp.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
Returns an URL to a given app, including the global base path. By default, the URL is relative (/basePath/app/my-app). Use the `absolute` option to generate an absolute url (http://host:port/basePath/app/my-app)
88

9-
Note that when generating absolute urls, the protocol, host and port are determined from the browser location.
9+
Note that when generating absolute urls, the origin (protocol, host and port) are determined from the browser's location.
1010

1111
<b>Signature:</b>
1212

docs/development/core/public/kibana-plugin-core-public.applicationstart.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ export interface ApplicationStart
2222

2323
| Method | Description |
2424
| --- | --- |
25-
| [getUrlForApp(appId, options)](./kibana-plugin-core-public.applicationstart.geturlforapp.md) | Returns an URL to a given app, including the global base path. By default, the URL is relative (/basePath/app/my-app). Use the <code>absolute</code> option to generate an absolute url (http://host:port/basePath/app/my-app)<!-- -->Note that when generating absolute urls, the protocol, host and port are determined from the browser location. |
25+
| [getUrlForApp(appId, options)](./kibana-plugin-core-public.applicationstart.geturlforapp.md) | Returns an URL to a given app, including the global base path. By default, the URL is relative (/basePath/app/my-app). Use the <code>absolute</code> option to generate an absolute url (http://host:port/basePath/app/my-app)<!-- -->Note that when generating absolute urls, the origin (protocol, host and port) are determined from the browser's location. |
2626
| [navigateToApp(appId, options)](./kibana-plugin-core-public.applicationstart.navigatetoapp.md) | Navigate to a given app |
27+
| [navigateToUrl(url)](./kibana-plugin-core-public.applicationstart.navigatetourl.md) | Navigate to given url, which can either be an absolute url or a relative path, in a SPA friendly way when possible.<!-- -->If all these criteria are true for the given url: - (only for absolute URLs) The origin of the URL matches the origin of the browser's current location - The pathname of the URL starts with the current basePath (eg. /mybasepath/s/my-space) - The pathname segment after the basePath matches any known application route (eg. /app/<id>/ or any application's <code>appRoute</code> configuration)<!-- -->Then a SPA navigation will be performed using <code>navigateToApp</code> using the corresponding application and path. Otherwise, fallback to a full page reload to navigate to the url using <code>window.location.assign</code> |
2728
| [registerMountContext(contextName, provider)](./kibana-plugin-core-public.applicationstart.registermountcontext.md) | Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. Deprecated, use [CoreSetup.getStartServices](./kibana-plugin-core-public.coresetup.getstartservices.md)<!-- -->. |
2829

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
2+
3+
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [ApplicationStart](./kibana-plugin-core-public.applicationstart.md) &gt; [navigateToUrl](./kibana-plugin-core-public.applicationstart.navigatetourl.md)
4+
5+
## ApplicationStart.navigateToUrl() method
6+
7+
Navigate to given url, which can either be an absolute url or a relative path, in a SPA friendly way when possible.
8+
9+
If all these criteria are true for the given url: - (only for absolute URLs) The origin of the URL matches the origin of the browser's current location - The pathname of the URL starts with the current basePath (eg. /mybasepath/s/my-space) - The pathname segment after the basePath matches any known application route (eg. /app/<id>/ or any application's `appRoute` configuration)
10+
11+
Then a SPA navigation will be performed using `navigateToApp` using the corresponding application and path. Otherwise, fallback to a full page reload to navigate to the url using `window.location.assign`
12+
13+
<b>Signature:</b>
14+
15+
```typescript
16+
navigateToUrl(url: string): Promise<void>;
17+
```
18+
19+
## Parameters
20+
21+
| Parameter | Type | Description |
22+
| --- | --- | --- |
23+
| url | <code>string</code> | an absolute url, or a relative path, to navigate to. |
24+
25+
<b>Returns:</b>
26+
27+
`Promise<void>`
28+
29+
## Example
30+
31+
32+
```ts
33+
// current url: `https://kibana:8080/base-path/s/my-space/app/dashboard`
34+
35+
// will call `application.navigateToApp('discover', { path: '/some-path?foo=bar'})`
36+
application.navigateToUrl('https://kibana:8080/base-path/s/my-space/app/discover/some-path?foo=bar')
37+
application.navigateToUrl('/base-path/s/my-space/app/discover/some-path?foo=bar')
38+
39+
// will perform a full page reload using `window.location.assign`
40+
application.navigateToUrl('https://elsewhere:8080/base-path/s/my-space/app/discover/some-path') // origin does not match
41+
application.navigateToUrl('/app/discover/some-path') // does not include the current basePath
42+
application.navigateToUrl('/base-path/s/my-space/app/unknown-app/some-path') // unknown application
43+
44+
```
45+

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@
176176
"deep-freeze-strict": "^1.1.1",
177177
"deepmerge": "^4.2.2",
178178
"del": "^5.1.0",
179-
"elastic-apm-node": "^3.2.0",
179+
"elastic-apm-node": "^3.6.0",
180180
"elasticsearch": "^16.7.0",
181181
"elasticsearch-browser": "^16.7.0",
182182
"execa": "^4.0.0",

src/core/public/application/application_service.mock.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ const createStartContractMock = (): jest.Mocked<ApplicationStart> => {
5050
currentAppId$: currentAppId$.asObservable(),
5151
capabilities: capabilitiesServiceMock.createStartContract().capabilities,
5252
navigateToApp: jest.fn(),
53+
navigateToUrl: jest.fn(),
5354
getUrlForApp: jest.fn(),
5455
registerMountContext: jest.fn(),
5556
};
@@ -65,6 +66,7 @@ const createInternalStartContractMock = (): jest.Mocked<InternalApplicationStart
6566
getComponent: jest.fn(),
6667
getUrlForApp: jest.fn(),
6768
navigateToApp: jest.fn().mockImplementation((appId) => currentAppId$.next(appId)),
69+
navigateToUrl: jest.fn(),
6870
registerMountContext: jest.fn(),
6971
};
7072
};

src/core/public/application/application_service.test.mocks.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,9 @@ export const createBrowserHistoryMock = jest.fn().mockReturnValue(MockHistory);
3434
jest.doMock('history', () => ({
3535
createBrowserHistory: createBrowserHistoryMock,
3636
}));
37+
38+
export const parseAppUrlMock = jest.fn();
39+
jest.doMock('./utils', () => ({
40+
...jest.requireActual('./utils'),
41+
parseAppUrl: parseAppUrlMock,
42+
}));

src/core/public/application/application_service.test.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717
* under the License.
1818
*/
1919

20+
import {
21+
MockCapabilitiesService,
22+
MockHistory,
23+
parseAppUrlMock,
24+
} from './application_service.test.mocks';
25+
2026
import { createElement } from 'react';
2127
import { BehaviorSubject, Subject } from 'rxjs';
2228
import { bufferCount, take, takeUntil } from 'rxjs/operators';
@@ -26,7 +32,6 @@ import { injectedMetadataServiceMock } from '../injected_metadata/injected_metad
2632
import { contextServiceMock } from '../context/context_service.mock';
2733
import { httpServiceMock } from '../http/http_service.mock';
2834
import { overlayServiceMock } from '../overlays/overlay_service.mock';
29-
import { MockCapabilitiesService, MockHistory } from './application_service.test.mocks';
3035
import { MockLifecycle } from './test_types';
3136
import { ApplicationService } from './application_service';
3237
import { App, AppNavLinkStatus, AppStatus, AppUpdater, LegacyApp } from './types';
@@ -61,6 +66,7 @@ describe('#setup()', () => {
6166
http,
6267
context: contextServiceMock.createSetupContract(),
6368
injectedMetadata: injectedMetadataServiceMock.createSetupContract(),
69+
redirectTo: jest.fn(),
6470
};
6571
setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(false);
6672
startDeps = { http, overlays: overlayServiceMock.createStartContract() };
@@ -174,6 +180,10 @@ describe('#setup()', () => {
174180
).toThrowErrorMatchingInlineSnapshot(
175181
`"Cannot register an application route that includes HTTP base path"`
176182
);
183+
184+
expect(() =>
185+
register(Symbol(), createApp({ id: 'app3', appRoute: '/base-path-i-am-not' }))
186+
).not.toThrow();
177187
});
178188
});
179189

@@ -462,12 +472,14 @@ describe('#setup()', () => {
462472
describe('#start()', () => {
463473
beforeEach(() => {
464474
MockHistory.push.mockReset();
475+
parseAppUrlMock.mockReset();
465476

466477
const http = httpServiceMock.createSetupContract({ basePath: '/base-path' });
467478
setupDeps = {
468479
http,
469480
context: contextServiceMock.createSetupContract(),
470481
injectedMetadata: injectedMetadataServiceMock.createSetupContract(),
482+
redirectTo: jest.fn(),
471483
};
472484
setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(false);
473485
startDeps = { http, overlays: overlayServiceMock.createStartContract() };
@@ -775,7 +787,6 @@ describe('#start()', () => {
775787
});
776788

777789
it('redirects when in legacyMode', async () => {
778-
setupDeps.redirectTo = jest.fn();
779790
setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(true);
780791
service.setup(setupDeps);
781792

@@ -881,7 +892,6 @@ describe('#start()', () => {
881892
it('sets window.location.href when navigating to legacy apps', async () => {
882893
setupDeps.http = httpServiceMock.createSetupContract({ basePath: '/test' });
883894
setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(true);
884-
setupDeps.redirectTo = jest.fn();
885895
service.setup(setupDeps);
886896

887897
const { navigateToApp } = await service.start(startDeps);
@@ -893,7 +903,6 @@ describe('#start()', () => {
893903
it('handles legacy apps with subapps', async () => {
894904
setupDeps.http = httpServiceMock.createSetupContract({ basePath: '/test' });
895905
setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(true);
896-
setupDeps.redirectTo = jest.fn();
897906

898907
const { registerLegacyApp } = service.setup(setupDeps);
899908

@@ -905,6 +914,30 @@ describe('#start()', () => {
905914
expect(setupDeps.redirectTo).toHaveBeenCalledWith('/test/app/baseApp');
906915
});
907916
});
917+
918+
describe('navigateToUrl', () => {
919+
it('calls `redirectTo` when the url is not parseable', async () => {
920+
parseAppUrlMock.mockReturnValue(undefined);
921+
service.setup(setupDeps);
922+
const { navigateToUrl } = await service.start(startDeps);
923+
924+
await navigateToUrl('/not-an-app-path');
925+
926+
expect(MockHistory.push).not.toHaveBeenCalled();
927+
expect(setupDeps.redirectTo).toHaveBeenCalledWith('/not-an-app-path');
928+
});
929+
930+
it('calls `navigateToApp` when the url is an internal app link', async () => {
931+
parseAppUrlMock.mockReturnValue({ app: 'foo', path: '/some-path' });
932+
service.setup(setupDeps);
933+
const { navigateToUrl } = await service.start(startDeps);
934+
935+
await navigateToUrl('/an-app-path');
936+
937+
expect(MockHistory.push).toHaveBeenCalledWith('/app/foo/some-path', undefined);
938+
expect(setupDeps.redirectTo).not.toHaveBeenCalled();
939+
});
940+
});
908941
});
909942

910943
describe('#stop()', () => {

src/core/public/application/application_service.tsx

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,14 @@ import {
4646
Mounter,
4747
} from './types';
4848
import { getLeaveAction, isConfirmAction } from './application_leave';
49-
import { appendAppPath } from './utils';
49+
import { appendAppPath, parseAppUrl, relativeToAbsolute } from './utils';
5050

5151
interface SetupDeps {
5252
context: ContextSetup;
5353
http: HttpSetup;
5454
injectedMetadata: InjectedMetadataSetup;
5555
history?: History<any>;
56-
/**
57-
* Only necessary for redirecting to legacy apps
58-
* @deprecated
59-
*/
56+
/** Used to redirect to external urls (and legacy apps) */
6057
redirectTo?: (path: string) => void;
6158
}
6259

@@ -109,6 +106,7 @@ export class ApplicationService {
109106
private history?: History<any>;
110107
private mountContext?: IContextContainer<AppMountDeprecated>;
111108
private navigate?: (url: string, state: any) => void;
109+
private redirectTo?: (url: string) => void;
112110

113111
public setup({
114112
context,
@@ -131,7 +129,7 @@ export class ApplicationService {
131129
this.navigate = (url, state) =>
132130
// basePath not needed here because `history` is configured with basename
133131
this.history ? this.history.push(url, state) : redirectTo(basePath.prepend(url));
134-
132+
this.redirectTo = redirectTo;
135133
this.mountContext = context.createContextContainer();
136134

137135
const registerStatusUpdater = (application: string, updater$: Observable<AppUpdater>) => {
@@ -179,7 +177,7 @@ export class ApplicationService {
179177
throw new Error(
180178
`An application is already registered with the appRoute "${app.appRoute}"`
181179
);
182-
} else if (basename && app.appRoute!.startsWith(basename)) {
180+
} else if (basename && app.appRoute!.startsWith(`${basename}/`)) {
183181
throw new Error('Cannot register an application route that includes HTTP base path');
184182
}
185183

@@ -208,7 +206,7 @@ export class ApplicationService {
208206
throw new Error('Applications cannot be registered after "setup"');
209207
} else if (this.apps.has(app.id)) {
210208
throw new Error(`An application is already registered with the id "${app.id}"`);
211-
} else if (basename && appRoute!.startsWith(basename)) {
209+
} else if (basename && appRoute!.startsWith(`${basename}/`)) {
212210
throw new Error('Cannot register an application route that includes HTTP base path');
213211
}
214212

@@ -278,6 +276,20 @@ export class ApplicationService {
278276
shareReplay(1)
279277
);
280278

279+
const navigateToApp: InternalApplicationStart['navigateToApp'] = async (
280+
appId,
281+
{ path, state }: { path?: string; state?: any } = {}
282+
) => {
283+
if (await this.shouldNavigate(overlays)) {
284+
if (path === undefined) {
285+
path = applications$.value.get(appId)?.defaultPath;
286+
}
287+
this.appLeaveHandlers.delete(this.currentAppId$.value!);
288+
this.navigate!(getAppUrl(availableMounters, appId, path), state);
289+
this.currentAppId$.next(appId);
290+
}
291+
};
292+
281293
return {
282294
applications$,
283295
capabilities,
@@ -294,14 +306,13 @@ export class ApplicationService {
294306
const relUrl = http.basePath.prepend(getAppUrl(availableMounters, appId, path));
295307
return absolute ? relativeToAbsolute(relUrl) : relUrl;
296308
},
297-
navigateToApp: async (appId, { path, state }: { path?: string; state?: any } = {}) => {
298-
if (await this.shouldNavigate(overlays)) {
299-
if (path === undefined) {
300-
path = applications$.value.get(appId)?.defaultPath;
301-
}
302-
this.appLeaveHandlers.delete(this.currentAppId$.value!);
303-
this.navigate!(getAppUrl(availableMounters, appId, path), state);
304-
this.currentAppId$.next(appId);
309+
navigateToApp,
310+
navigateToUrl: async (url) => {
311+
const appInfo = parseAppUrl(url, http.basePath, this.apps);
312+
if (appInfo) {
313+
return navigateToApp(appInfo.app, { path: appInfo.path });
314+
} else {
315+
return this.redirectTo!(url);
305316
}
306317
},
307318
getComponent: () => {
@@ -388,10 +399,3 @@ const updateStatus = <T extends AppBase>(app: T, statusUpdaters: AppUpdaterWrapp
388399
...changes,
389400
};
390401
};
391-
392-
function relativeToAbsolute(url: string) {
393-
// convert all link urls to absolute urls
394-
const a = document.createElement('a');
395-
a.setAttribute('href', url);
396-
return a.href;
397-
}

src/core/public/application/types.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -659,12 +659,41 @@ export interface ApplicationStart {
659659
*/
660660
navigateToApp(appId: string, options?: { path?: string; state?: any }): Promise<void>;
661661

662+
/**
663+
* Navigate to given url, which can either be an absolute url or a relative path, in a SPA friendly way when possible.
664+
*
665+
* If all these criteria are true for the given url:
666+
* - (only for absolute URLs) The origin of the URL matches the origin of the browser's current location
667+
* - The pathname of the URL starts with the current basePath (eg. /mybasepath/s/my-space)
668+
* - The pathname segment after the basePath matches any known application route (eg. /app/<id>/ or any application's `appRoute` configuration)
669+
*
670+
* Then a SPA navigation will be performed using `navigateToApp` using the corresponding application and path.
671+
* Otherwise, fallback to a full page reload to navigate to the url using `window.location.assign`
672+
*
673+
* @example
674+
* ```ts
675+
* // current url: `https://kibana:8080/base-path/s/my-space/app/dashboard`
676+
*
677+
* // will call `application.navigateToApp('discover', { path: '/some-path?foo=bar'})`
678+
* application.navigateToUrl('https://kibana:8080/base-path/s/my-space/app/discover/some-path?foo=bar')
679+
* application.navigateToUrl('/base-path/s/my-space/app/discover/some-path?foo=bar')
680+
*
681+
* // will perform a full page reload using `window.location.assign`
682+
* application.navigateToUrl('https://elsewhere:8080/base-path/s/my-space/app/discover/some-path') // origin does not match
683+
* application.navigateToUrl('/app/discover/some-path') // does not include the current basePath
684+
* application.navigateToUrl('/base-path/s/my-space/app/unknown-app/some-path') // unknown application
685+
* ```
686+
*
687+
* @param url - an absolute url, or a relative path, to navigate to.
688+
*/
689+
navigateToUrl(url: string): Promise<void>;
690+
662691
/**
663692
* Returns an URL to a given app, including the global base path.
664693
* By default, the URL is relative (/basePath/app/my-app).
665694
* Use the `absolute` option to generate an absolute url (http://host:port/basePath/app/my-app)
666695
*
667-
* Note that when generating absolute urls, the protocol, host and port are determined from the browser location.
696+
* Note that when generating absolute urls, the origin (protocol, host and port) are determined from the browser's location.
668697
*
669698
* @param appId
670699
* @param options.path - optional path inside application to deep link to
@@ -677,7 +706,6 @@ export interface ApplicationStart {
677706
* plugin that registered this context. Deprecated, use {@link CoreSetup.getStartServices}.
678707
*
679708
* @deprecated
680-
* @param pluginOpaqueId - The opaque ID of the plugin that is registering the context.
681709
* @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to.
682710
* @param provider - A {@link IContextProvider} function
683711
*/
@@ -696,7 +724,7 @@ export interface ApplicationStart {
696724
export interface InternalApplicationStart
697725
extends Pick<
698726
ApplicationStart,
699-
'capabilities' | 'navigateToApp' | 'getUrlForApp' | 'currentAppId$'
727+
'capabilities' | 'navigateToApp' | 'navigateToUrl' | 'getUrlForApp' | 'currentAppId$'
700728
> {
701729
/**
702730
* Apps available based on the current capabilities.

0 commit comments

Comments
 (0)