mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Shared UX] Redirect App Link to package (#131575)
* [Shared UX] Redirect App Link to package * Fix types
This commit is contained in:
parent
53d170aeca
commit
5bdad23a7e
35 changed files with 1273 additions and 850 deletions
|
@ -180,6 +180,7 @@
|
|||
"@kbn/server-route-repository": "link:bazel-bin/packages/kbn-server-route-repository",
|
||||
"@kbn/shared-ux-button-exit-full-screen": "link:bazel-bin/packages/shared-ux/button/exit_full_screen",
|
||||
"@kbn/shared-ux-components": "link:bazel-bin/packages/kbn-shared-ux-components",
|
||||
"@kbn/shared-ux-link-redirect-app": "link:bazel-bin/packages/shared-ux/link/redirect_app",
|
||||
"@kbn/shared-ux-services": "link:bazel-bin/packages/kbn-shared-ux-services",
|
||||
"@kbn/shared-ux-storybook": "link:bazel-bin/packages/kbn-shared-ux-storybook",
|
||||
"@kbn/shared-ux-utility": "link:bazel-bin/packages/kbn-shared-ux-utility",
|
||||
|
@ -667,6 +668,7 @@
|
|||
"@types/kbn__server-route-repository": "link:bazel-bin/packages/kbn-server-route-repository/npm_module_types",
|
||||
"@types/kbn__shared-ux-button-exit-full-screen": "link:bazel-bin/packages/shared-ux/button/exit_full_screen/npm_module_types",
|
||||
"@types/kbn__shared-ux-components": "link:bazel-bin/packages/kbn-shared-ux-components/npm_module_types",
|
||||
"@types/kbn__shared-ux-link-redirect-app": "link:bazel-bin/packages/shared-ux/link/redirect_app/npm_module_types",
|
||||
"@types/kbn__shared-ux-services": "link:bazel-bin/packages/kbn-shared-ux-services/npm_module_types",
|
||||
"@types/kbn__shared-ux-storybook": "link:bazel-bin/packages/kbn-shared-ux-storybook/npm_module_types",
|
||||
"@types/kbn__shared-ux-utility": "link:bazel-bin/packages/kbn-shared-ux-utility/npm_module_types",
|
||||
|
|
|
@ -110,6 +110,7 @@ filegroup(
|
|||
"//packages/kbn-utility-types:build",
|
||||
"//packages/kbn-utils:build",
|
||||
"//packages/shared-ux/button/exit_full_screen:build",
|
||||
"//packages/shared-ux/link/redirect_app:build",
|
||||
],
|
||||
)
|
||||
|
||||
|
@ -203,6 +204,7 @@ filegroup(
|
|||
"//packages/kbn-utility-types:build_types",
|
||||
"//packages/kbn-utils:build_types",
|
||||
"//packages/shared-ux/button/exit_full_screen:build_types",
|
||||
"//packages/shared-ux/link/redirect_app:build_types",
|
||||
],
|
||||
)
|
||||
|
||||
|
|
|
@ -40,8 +40,9 @@ NPM_MODULE_EXTRA_FILES = [
|
|||
# "@npm//name-of-package"
|
||||
# eg. "@npm//lodash"
|
||||
RUNTIME_DEPS = [
|
||||
"//packages/kbn-i18n",
|
||||
"//packages/kbn-i18n-react",
|
||||
"//packages/kbn-i18n",
|
||||
"//packages/shared-ux/link/redirect_app",
|
||||
"//packages/kbn-shared-ux-services",
|
||||
"//packages/kbn-shared-ux-storybook",
|
||||
"//packages/kbn-shared-ux-utility",
|
||||
|
@ -51,6 +52,7 @@ RUNTIME_DEPS = [
|
|||
"@npm//classnames",
|
||||
"@npm//react-use",
|
||||
"@npm//react",
|
||||
"@npm//rxjs",
|
||||
"@npm//url-loader",
|
||||
]
|
||||
|
||||
|
@ -64,12 +66,13 @@ RUNTIME_DEPS = [
|
|||
#
|
||||
# References to NPM packages work the same as RUNTIME_DEPS
|
||||
TYPES_DEPS = [
|
||||
"//packages/kbn-i18n:npm_module_types",
|
||||
"//packages/kbn-ambient-ui-types",
|
||||
"//packages/kbn-i18n-react:npm_module_types",
|
||||
"//packages/kbn-i18n:npm_module_types",
|
||||
"//packages/shared-ux/link/redirect_app:npm_module_types",
|
||||
"//packages/kbn-shared-ux-services:npm_module_types",
|
||||
"//packages/kbn-shared-ux-storybook:npm_module_types",
|
||||
"//packages/kbn-shared-ux-utility:npm_module_types",
|
||||
"//packages/kbn-ambient-ui-types",
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/jest",
|
||||
"@npm//@types/react",
|
||||
|
@ -78,6 +81,7 @@ TYPES_DEPS = [
|
|||
"@npm//@emotion/css",
|
||||
"@npm//@elastic/eui",
|
||||
"@npm//react-use",
|
||||
"@npm//rxjs",
|
||||
]
|
||||
|
||||
jsts_transpiler(
|
||||
|
|
|
@ -15,8 +15,6 @@ export const LazyToolbarButton = React.lazy(() =>
|
|||
}))
|
||||
);
|
||||
|
||||
export const RedirectAppLinks = React.lazy(() => import('./redirect_app_links'));
|
||||
|
||||
/**
|
||||
* A `ToolbarButton` component that is wrapped by the `withSuspense` HOC. This component can
|
||||
* be used directly by consumers and will load the `LazyToolbarButton` component lazily with
|
||||
|
|
|
@ -1,112 +1,48 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ElasticAgentCardComponent props button 1`] = `
|
||||
<RedirectAppLinks
|
||||
currentAppId$={
|
||||
Observable {
|
||||
"source": Subject {
|
||||
"closed": false,
|
||||
"currentObservers": null,
|
||||
"hasError": false,
|
||||
"isStopped": false,
|
||||
"observers": Array [],
|
||||
"thrownError": null,
|
||||
},
|
||||
}
|
||||
}
|
||||
navigateToUrl={[MockFunction]}
|
||||
>
|
||||
<NoDataCard
|
||||
button="Button"
|
||||
description="Use Elastic Agent for a simple, unified way to collect data from your machines."
|
||||
image="test-file-stub"
|
||||
title="Add Elastic Agent"
|
||||
/>
|
||||
</RedirectAppLinks>
|
||||
<NoDataCard
|
||||
button="Button"
|
||||
description="Use Elastic Agent for a simple, unified way to collect data from your machines."
|
||||
image="test-file-stub"
|
||||
title="Add Elastic Agent"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`ElasticAgentCardComponent props href 1`] = `
|
||||
<RedirectAppLinks
|
||||
currentAppId$={
|
||||
Observable {
|
||||
"source": Subject {
|
||||
"closed": false,
|
||||
"currentObservers": null,
|
||||
"hasError": false,
|
||||
"isStopped": false,
|
||||
"observers": Array [],
|
||||
"thrownError": null,
|
||||
},
|
||||
}
|
||||
}
|
||||
navigateToUrl={[MockFunction]}
|
||||
>
|
||||
<NoDataCard
|
||||
description="Use Elastic Agent for a simple, unified way to collect data from your machines."
|
||||
href="some path"
|
||||
image="test-file-stub"
|
||||
title="Add Elastic Agent"
|
||||
/>
|
||||
</RedirectAppLinks>
|
||||
<NoDataCard
|
||||
description="Use Elastic Agent for a simple, unified way to collect data from your machines."
|
||||
href="some path"
|
||||
image="test-file-stub"
|
||||
title="Add Elastic Agent"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`ElasticAgentCardComponent renders 1`] = `
|
||||
<RedirectAppLinks
|
||||
currentAppId$={
|
||||
Observable {
|
||||
"source": Subject {
|
||||
"closed": false,
|
||||
"currentObservers": null,
|
||||
"hasError": false,
|
||||
"isStopped": false,
|
||||
"observers": Array [],
|
||||
"thrownError": null,
|
||||
},
|
||||
}
|
||||
}
|
||||
navigateToUrl={[MockFunction]}
|
||||
>
|
||||
<NoDataCard
|
||||
description="Use Elastic Agent for a simple, unified way to collect data from your machines."
|
||||
image="test-file-stub"
|
||||
title="Add Elastic Agent"
|
||||
/>
|
||||
</RedirectAppLinks>
|
||||
<NoDataCard
|
||||
description="Use Elastic Agent for a simple, unified way to collect data from your machines."
|
||||
image="test-file-stub"
|
||||
title="Add Elastic Agent"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`ElasticAgentCardComponent renders with canAccessFleet false 1`] = `
|
||||
<RedirectAppLinks
|
||||
currentAppId$={
|
||||
Observable {
|
||||
"source": Subject {
|
||||
"closed": false,
|
||||
"currentObservers": null,
|
||||
"hasError": false,
|
||||
"isStopped": false,
|
||||
"observers": Array [],
|
||||
"thrownError": null,
|
||||
},
|
||||
}
|
||||
<NoDataCard
|
||||
description={
|
||||
<EuiTextColor
|
||||
color="default"
|
||||
>
|
||||
This integration is not yet enabled. Your administrator has the required permissions to turn it on.
|
||||
</EuiTextColor>
|
||||
}
|
||||
navigateToUrl={[MockFunction]}
|
||||
>
|
||||
<NoDataCard
|
||||
description={
|
||||
<EuiTextColor
|
||||
color="default"
|
||||
>
|
||||
This integration is not yet enabled. Your administrator has the required permissions to turn it on.
|
||||
</EuiTextColor>
|
||||
}
|
||||
image="test-file-stub"
|
||||
isDisabled={true}
|
||||
title={
|
||||
<EuiTextColor
|
||||
color="default"
|
||||
>
|
||||
Contact your administrator
|
||||
</EuiTextColor>
|
||||
}
|
||||
/>
|
||||
</RedirectAppLinks>
|
||||
image="test-file-stub"
|
||||
isDisabled={true}
|
||||
title={
|
||||
<EuiTextColor
|
||||
color="default"
|
||||
>
|
||||
Contact your administrator
|
||||
</EuiTextColor>
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
|
|
@ -4,7 +4,9 @@ exports[`ElasticAgentCard renders 1`] = `
|
|||
<SharedUxServicesProvider
|
||||
application={
|
||||
Object {
|
||||
"currentAppId$": Observable {},
|
||||
"currentAppId$": Observable {
|
||||
"_subscribe": [Function],
|
||||
},
|
||||
"navigateToUrl": [Function],
|
||||
}
|
||||
}
|
||||
|
@ -157,223 +159,235 @@ exports[`ElasticAgentCard renders 1`] = `
|
|||
}
|
||||
>
|
||||
<ElasticAgentCard>
|
||||
<ElasticAgentCardComponent
|
||||
canAccessFleet={true}
|
||||
currentAppId$={Observable {}}
|
||||
href="/app/integrations/browse"
|
||||
<RedirectAppLinks
|
||||
currentAppId="abc123"
|
||||
navigateToUrl={[Function]}
|
||||
>
|
||||
<RedirectAppLinks
|
||||
currentAppId$={Observable {}}
|
||||
<RedirectAppLinksProvider
|
||||
currentAppId="abc123"
|
||||
navigateToUrl={[Function]}
|
||||
>
|
||||
<div>
|
||||
<NoDataCard
|
||||
description="Use Elastic Agent for a simple, unified way to collect data from your machines."
|
||||
href="/app/integrations/browse"
|
||||
image="test-file-stub"
|
||||
title="Add Elastic Agent"
|
||||
<RedirectAppLinks>
|
||||
<RedirectAppLinks
|
||||
currentAppId="abc123"
|
||||
navigateToUrl={[Function]}
|
||||
>
|
||||
<EuiCard
|
||||
css={
|
||||
Object {
|
||||
"marginInline": "auto",
|
||||
"maxWidth": 400,
|
||||
}
|
||||
}
|
||||
description="Use Elastic Agent for a simple, unified way to collect data from your machines."
|
||||
footer={
|
||||
<EuiButton
|
||||
fill={true}
|
||||
>
|
||||
Add Elastic Agent
|
||||
</EuiButton>
|
||||
}
|
||||
href="/app/integrations/browse"
|
||||
image="test-file-stub"
|
||||
paddingSize="l"
|
||||
title="Add Elastic Agent"
|
||||
<div
|
||||
onClick={[Function]}
|
||||
>
|
||||
<EuiPanel
|
||||
css="unknown styles"
|
||||
element="div"
|
||||
hasShadow={true}
|
||||
onClick={[Function]}
|
||||
paddingSize="l"
|
||||
<ElasticAgentCardComponent
|
||||
canAccessFleet={true}
|
||||
href="/app/integrations/browse"
|
||||
>
|
||||
<Insertion
|
||||
cache={
|
||||
Object {
|
||||
"insert": [Function],
|
||||
"inserted": Object {
|
||||
"1hu4pg0-EuiCard": true,
|
||||
},
|
||||
"key": "css",
|
||||
"nonce": undefined,
|
||||
"registered": Object {
|
||||
"css-1hu4pg0-EuiCard": "max-width:400px;margin-inline:auto;;label:EuiCard;",
|
||||
},
|
||||
"sheet": StyleSheet {
|
||||
"_alreadyInsertedOrderInsensitiveRule": true,
|
||||
"_insertTag": [Function],
|
||||
"before": null,
|
||||
"container": <head>
|
||||
<style
|
||||
data-emotion="css"
|
||||
data-s=""
|
||||
>
|
||||
|
||||
.css-1hu4pg0-EuiCard{max-width:400px;margin-inline:auto;}
|
||||
</style>
|
||||
<style
|
||||
data-styled="active"
|
||||
data-styled-version="5.1.0"
|
||||
/>
|
||||
</head>,
|
||||
"ctr": 1,
|
||||
"insertionPoint": undefined,
|
||||
"isSpeedy": false,
|
||||
"key": "css",
|
||||
"nonce": undefined,
|
||||
"prepend": undefined,
|
||||
"tags": Array [
|
||||
<style
|
||||
data-emotion="css"
|
||||
data-s=""
|
||||
>
|
||||
|
||||
.css-1hu4pg0-EuiCard{max-width:400px;margin-inline:auto;}
|
||||
</style>,
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
isStringTag={false}
|
||||
serialized={
|
||||
Object {
|
||||
"map": undefined,
|
||||
"name": "1hu4pg0-EuiCard",
|
||||
"next": undefined,
|
||||
"styles": "max-width:400px;margin-inline:auto;;label:EuiCard;",
|
||||
"toString": [Function],
|
||||
}
|
||||
}
|
||||
/>
|
||||
<EuiPanel
|
||||
className="euiCard euiCard--centerAligned euiCard--isClickable css-1hu4pg0-EuiCard"
|
||||
element="div"
|
||||
hasShadow={true}
|
||||
onClick={[Function]}
|
||||
paddingSize="l"
|
||||
<NoDataCard
|
||||
description="Use Elastic Agent for a simple, unified way to collect data from your machines."
|
||||
href="/app/integrations/browse"
|
||||
image="test-file-stub"
|
||||
title="Add Elastic Agent"
|
||||
>
|
||||
<div
|
||||
className="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--isClickable euiCard euiCard--centerAligned euiCard--isClickable css-1hu4pg0-EuiCard"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<div
|
||||
className="euiCard__top"
|
||||
>
|
||||
<div
|
||||
className="euiCard__image"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="euiCard__content"
|
||||
>
|
||||
<EuiTitle
|
||||
className="euiCard__title"
|
||||
id="generated-idTitle"
|
||||
size="s"
|
||||
>
|
||||
<span
|
||||
className="euiTitle euiTitle--small euiCard__title"
|
||||
id="generated-idTitle"
|
||||
>
|
||||
<a
|
||||
aria-describedby="generated-idDescription"
|
||||
className="euiCard__titleAnchor"
|
||||
href="/app/integrations/browse"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Add Elastic Agent
|
||||
</a>
|
||||
</span>
|
||||
</EuiTitle>
|
||||
<EuiText
|
||||
className="euiCard__description"
|
||||
id="generated-idDescription"
|
||||
size="s"
|
||||
>
|
||||
<div
|
||||
className="euiText euiText--small euiCard__description"
|
||||
id="generated-idDescription"
|
||||
>
|
||||
<p>
|
||||
Use Elastic Agent for a simple, unified way to collect data from your machines.
|
||||
</p>
|
||||
</div>
|
||||
</EuiText>
|
||||
</div>
|
||||
<div
|
||||
className="euiCard__footer"
|
||||
>
|
||||
<EuiCard
|
||||
css={
|
||||
Object {
|
||||
"marginInline": "auto",
|
||||
"maxWidth": 400,
|
||||
}
|
||||
}
|
||||
description="Use Elastic Agent for a simple, unified way to collect data from your machines."
|
||||
footer={
|
||||
<EuiButton
|
||||
fill={true}
|
||||
>
|
||||
<EuiButtonDisplay
|
||||
baseClassName="euiButton"
|
||||
disabled={false}
|
||||
element="button"
|
||||
fill={true}
|
||||
isDisabled={false}
|
||||
type="button"
|
||||
Add Elastic Agent
|
||||
</EuiButton>
|
||||
}
|
||||
href="/app/integrations/browse"
|
||||
image="test-file-stub"
|
||||
paddingSize="l"
|
||||
title="Add Elastic Agent"
|
||||
>
|
||||
<EuiPanel
|
||||
css="unknown styles"
|
||||
element="div"
|
||||
hasShadow={true}
|
||||
onClick={[Function]}
|
||||
paddingSize="l"
|
||||
>
|
||||
<Insertion
|
||||
cache={
|
||||
Object {
|
||||
"insert": [Function],
|
||||
"inserted": Object {
|
||||
"1hu4pg0-EuiCard": true,
|
||||
},
|
||||
"key": "css",
|
||||
"nonce": undefined,
|
||||
"registered": Object {
|
||||
"css-1hu4pg0-EuiCard": "max-width:400px;margin-inline:auto;;label:EuiCard;",
|
||||
},
|
||||
"sheet": StyleSheet {
|
||||
"_alreadyInsertedOrderInsensitiveRule": true,
|
||||
"_insertTag": [Function],
|
||||
"before": null,
|
||||
"container": <head>
|
||||
<style
|
||||
data-emotion="css"
|
||||
data-s=""
|
||||
>
|
||||
|
||||
.css-1hu4pg0-EuiCard{max-width:400px;margin-inline:auto;}
|
||||
</style>
|
||||
<style
|
||||
data-styled="active"
|
||||
data-styled-version="5.1.0"
|
||||
/>
|
||||
</head>,
|
||||
"ctr": 1,
|
||||
"insertionPoint": undefined,
|
||||
"isSpeedy": false,
|
||||
"key": "css",
|
||||
"nonce": undefined,
|
||||
"prepend": undefined,
|
||||
"tags": Array [
|
||||
<style
|
||||
data-emotion="css"
|
||||
data-s=""
|
||||
>
|
||||
|
||||
.css-1hu4pg0-EuiCard{max-width:400px;margin-inline:auto;}
|
||||
</style>,
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
isStringTag={false}
|
||||
serialized={
|
||||
Object {
|
||||
"map": undefined,
|
||||
"name": "1hu4pg0-EuiCard",
|
||||
"next": undefined,
|
||||
"styles": "max-width:400px;margin-inline:auto;;label:EuiCard;",
|
||||
"toString": [Function],
|
||||
}
|
||||
}
|
||||
/>
|
||||
<EuiPanel
|
||||
className="euiCard euiCard--centerAligned euiCard--isClickable css-1hu4pg0-EuiCard"
|
||||
element="div"
|
||||
hasShadow={true}
|
||||
onClick={[Function]}
|
||||
paddingSize="l"
|
||||
>
|
||||
<div
|
||||
className="euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--isClickable euiCard euiCard--centerAligned euiCard--isClickable css-1hu4pg0-EuiCard"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<button
|
||||
className="euiButton euiButton--primary euiButton--fill"
|
||||
disabled={false}
|
||||
style={
|
||||
Object {
|
||||
"minWidth": undefined,
|
||||
}
|
||||
}
|
||||
type="button"
|
||||
<div
|
||||
className="euiCard__top"
|
||||
>
|
||||
<EuiButtonContent
|
||||
className="euiButton__content"
|
||||
iconSide="left"
|
||||
textProps={
|
||||
Object {
|
||||
"className": "euiButton__text",
|
||||
}
|
||||
}
|
||||
<div
|
||||
className="euiCard__image"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="euiCard__content"
|
||||
>
|
||||
<EuiTitle
|
||||
className="euiCard__title"
|
||||
id="generated-idTitle"
|
||||
size="s"
|
||||
>
|
||||
<span
|
||||
className="euiButtonContent euiButton__content"
|
||||
className="euiTitle euiTitle--small euiCard__title"
|
||||
id="generated-idTitle"
|
||||
>
|
||||
<span
|
||||
className="euiButton__text"
|
||||
<a
|
||||
aria-describedby="generated-idDescription"
|
||||
className="euiCard__titleAnchor"
|
||||
href="/app/integrations/browse"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Add Elastic Agent
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
</EuiButtonContent>
|
||||
</button>
|
||||
</EuiButtonDisplay>
|
||||
</EuiButton>
|
||||
</div>
|
||||
</div>
|
||||
</EuiPanel>
|
||||
</EuiPanel>
|
||||
</EuiCard>
|
||||
</NoDataCard>
|
||||
</div>
|
||||
</RedirectAppLinks>
|
||||
</ElasticAgentCardComponent>
|
||||
</EuiTitle>
|
||||
<EuiText
|
||||
className="euiCard__description"
|
||||
id="generated-idDescription"
|
||||
size="s"
|
||||
>
|
||||
<div
|
||||
className="euiText euiText--small euiCard__description"
|
||||
id="generated-idDescription"
|
||||
>
|
||||
<p>
|
||||
Use Elastic Agent for a simple, unified way to collect data from your machines.
|
||||
</p>
|
||||
</div>
|
||||
</EuiText>
|
||||
</div>
|
||||
<div
|
||||
className="euiCard__footer"
|
||||
>
|
||||
<EuiButton
|
||||
fill={true}
|
||||
>
|
||||
<EuiButtonDisplay
|
||||
baseClassName="euiButton"
|
||||
disabled={false}
|
||||
element="button"
|
||||
fill={true}
|
||||
isDisabled={false}
|
||||
type="button"
|
||||
>
|
||||
<button
|
||||
className="euiButton euiButton--primary euiButton--fill"
|
||||
disabled={false}
|
||||
style={
|
||||
Object {
|
||||
"minWidth": undefined,
|
||||
}
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
<EuiButtonContent
|
||||
className="euiButton__content"
|
||||
iconSide="left"
|
||||
textProps={
|
||||
Object {
|
||||
"className": "euiButton__text",
|
||||
}
|
||||
}
|
||||
>
|
||||
<span
|
||||
className="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
className="euiButton__text"
|
||||
>
|
||||
Add Elastic Agent
|
||||
</span>
|
||||
</span>
|
||||
</EuiButtonContent>
|
||||
</button>
|
||||
</EuiButtonDisplay>
|
||||
</EuiButton>
|
||||
</div>
|
||||
</div>
|
||||
</EuiPanel>
|
||||
</EuiPanel>
|
||||
</EuiCard>
|
||||
</NoDataCard>
|
||||
</ElasticAgentCardComponent>
|
||||
</div>
|
||||
</RedirectAppLinks>
|
||||
</RedirectAppLinks>
|
||||
</RedirectAppLinksProvider>
|
||||
</RedirectAppLinks>
|
||||
</ElasticAgentCard>
|
||||
</SharedUxServicesProvider>
|
||||
`;
|
||||
|
|
|
@ -10,31 +10,15 @@ import { shallow } from 'enzyme';
|
|||
import React from 'react';
|
||||
import { ElasticAgentCardComponent } from './elastic_agent_card.component';
|
||||
import { NoDataCard } from './no_data_card';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
describe('ElasticAgentCardComponent', () => {
|
||||
const navigateToUrl = jest.fn();
|
||||
const currentAppId$ = new Subject<string | undefined>().asObservable();
|
||||
|
||||
test('renders', () => {
|
||||
const component = shallow(
|
||||
<ElasticAgentCardComponent
|
||||
canAccessFleet={true}
|
||||
navigateToUrl={navigateToUrl}
|
||||
currentAppId$={currentAppId$}
|
||||
/>
|
||||
);
|
||||
const component = shallow(<ElasticAgentCardComponent canAccessFleet={true} />);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('renders with canAccessFleet false', () => {
|
||||
const component = shallow(
|
||||
<ElasticAgentCardComponent
|
||||
canAccessFleet={false}
|
||||
navigateToUrl={navigateToUrl}
|
||||
currentAppId$={currentAppId$}
|
||||
/>
|
||||
);
|
||||
const component = shallow(<ElasticAgentCardComponent canAccessFleet={false} />);
|
||||
expect(component.find(NoDataCard).props().isDisabled).toBe(true);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
@ -42,12 +26,7 @@ describe('ElasticAgentCardComponent', () => {
|
|||
describe('props', () => {
|
||||
test('button', () => {
|
||||
const component = shallow(
|
||||
<ElasticAgentCardComponent
|
||||
button="Button"
|
||||
canAccessFleet={true}
|
||||
navigateToUrl={navigateToUrl}
|
||||
currentAppId$={currentAppId$}
|
||||
/>
|
||||
<ElasticAgentCardComponent button="Button" canAccessFleet={true} />
|
||||
);
|
||||
expect(component.find(NoDataCard).props().button).toBe('Button');
|
||||
expect(component).toMatchSnapshot();
|
||||
|
@ -55,12 +34,7 @@ describe('ElasticAgentCardComponent', () => {
|
|||
|
||||
test('href', () => {
|
||||
const component = shallow(
|
||||
<ElasticAgentCardComponent
|
||||
canAccessFleet={true}
|
||||
href={'some path'}
|
||||
navigateToUrl={navigateToUrl}
|
||||
currentAppId$={currentAppId$}
|
||||
/>
|
||||
<ElasticAgentCardComponent canAccessFleet={true} href={'some path'} />
|
||||
);
|
||||
expect(component.find(NoDataCard).props().href).toBe('some path');
|
||||
expect(component).toMatchSnapshot();
|
||||
|
|
|
@ -9,16 +9,12 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiTextColor } from '@elastic/eui';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ElasticAgentCardProps } from './types';
|
||||
import { NoDataCard } from './no_data_card';
|
||||
import ElasticAgentCardIllustration from './assets/elastic_agent_card.svg';
|
||||
import { RedirectAppLinks } from '../../../redirect_app_links';
|
||||
|
||||
export type ElasticAgentCardComponentProps = ElasticAgentCardProps & {
|
||||
canAccessFleet: boolean;
|
||||
navigateToUrl: (url: string) => Promise<void>;
|
||||
currentAppId$: Observable<string | undefined>;
|
||||
};
|
||||
|
||||
const noPermissionTitle = i18n.translate(
|
||||
|
@ -54,32 +50,19 @@ const elasticAgentCardDescription = i18n.translate(
|
|||
*/
|
||||
export const ElasticAgentCardComponent: FunctionComponent<ElasticAgentCardComponentProps> = ({
|
||||
canAccessFleet,
|
||||
title,
|
||||
navigateToUrl,
|
||||
currentAppId$,
|
||||
title = elasticAgentCardTitle,
|
||||
...cardRest
|
||||
}) => {
|
||||
const noAccessCard = (
|
||||
<NoDataCard
|
||||
image={ElasticAgentCardIllustration}
|
||||
title={<EuiTextColor color="default">{noPermissionTitle}</EuiTextColor>}
|
||||
description={<EuiTextColor color="default">{noPermissionDescription}</EuiTextColor>}
|
||||
isDisabled
|
||||
{...cardRest}
|
||||
/>
|
||||
);
|
||||
const card = (
|
||||
<NoDataCard
|
||||
image={ElasticAgentCardIllustration}
|
||||
title={title || elasticAgentCardTitle}
|
||||
description={elasticAgentCardDescription}
|
||||
{...cardRest}
|
||||
/>
|
||||
);
|
||||
const props = canAccessFleet
|
||||
? {
|
||||
title,
|
||||
description: elasticAgentCardDescription,
|
||||
}
|
||||
: {
|
||||
title: <EuiTextColor color="default">{noPermissionTitle}</EuiTextColor>,
|
||||
description: <EuiTextColor color="default">{noPermissionDescription}</EuiTextColor>,
|
||||
isDisabled: true,
|
||||
};
|
||||
|
||||
return (
|
||||
<RedirectAppLinks navigateToUrl={navigateToUrl} currentAppId$={currentAppId$}>
|
||||
{canAccessFleet ? card : noAccessCard}
|
||||
</RedirectAppLinks>
|
||||
);
|
||||
return <NoDataCard image={ElasticAgentCardIllustration} {...props} {...cardRest} />;
|
||||
};
|
||||
|
|
|
@ -7,29 +7,23 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { applicationServiceFactory } from '@kbn/shared-ux-storybook';
|
||||
|
||||
import {
|
||||
ElasticAgentCardComponent,
|
||||
ElasticAgentCardComponentProps,
|
||||
ElasticAgentCardComponent as Component,
|
||||
ElasticAgentCardComponentProps as ComponentProps,
|
||||
} from './elastic_agent_card.component';
|
||||
|
||||
import { ElasticAgentCard } from './elastic_agent_card';
|
||||
|
||||
export default {
|
||||
title: 'Page Template/No Data/Elastic Agent Data Card',
|
||||
description: 'A solution-specific wrapper around NoDataCard, to be used on NoData page',
|
||||
};
|
||||
|
||||
type Params = Pick<ElasticAgentCardComponentProps, 'canAccessFleet'>;
|
||||
type Params = Pick<ComponentProps, 'canAccessFleet'>;
|
||||
|
||||
export const PureComponent = (params: Params) => {
|
||||
const { currentAppId$, navigateToUrl } = applicationServiceFactory();
|
||||
return (
|
||||
<ElasticAgentCardComponent
|
||||
{...params}
|
||||
currentAppId$={currentAppId$}
|
||||
navigateToUrl={navigateToUrl}
|
||||
/>
|
||||
);
|
||||
return <Component {...params} />;
|
||||
};
|
||||
|
||||
PureComponent.argTypes = {
|
||||
|
@ -38,3 +32,7 @@ PureComponent.argTypes = {
|
|||
defaultValue: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const ConnectedComponent = () => {
|
||||
return <ElasticAgentCard href="#" />;
|
||||
};
|
||||
|
|
|
@ -6,8 +6,10 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useApplication, useHttp, usePermissions } from '@kbn/shared-ux-services';
|
||||
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
|
||||
import { ElasticAgentCardProps } from './types';
|
||||
import { ElasticAgentCardComponent } from './elastic_agent_card.component';
|
||||
|
@ -16,27 +18,28 @@ export const ElasticAgentCard = (props: ElasticAgentCardProps) => {
|
|||
const { canAccessFleet } = usePermissions();
|
||||
const { addBasePath } = useHttp();
|
||||
const { navigateToUrl, currentAppId$ } = useApplication();
|
||||
const currentAppId = useObservable(currentAppId$);
|
||||
|
||||
const createHref = () => {
|
||||
const { href, category } = props;
|
||||
if (href) {
|
||||
return href;
|
||||
const { href: srcHref, category } = props;
|
||||
|
||||
const href = useMemo(() => {
|
||||
if (srcHref) {
|
||||
return srcHref;
|
||||
}
|
||||
|
||||
// TODO: get this URL from a locator
|
||||
const prefix = '/app/integrations/browse';
|
||||
|
||||
if (category) {
|
||||
return addBasePath(`${prefix}/${category}`);
|
||||
}
|
||||
|
||||
return addBasePath(prefix);
|
||||
};
|
||||
}, [addBasePath, srcHref, category]);
|
||||
|
||||
return (
|
||||
<ElasticAgentCardComponent
|
||||
{...props}
|
||||
href={createHref()}
|
||||
canAccessFleet={canAccessFleet}
|
||||
navigateToUrl={navigateToUrl}
|
||||
currentAppId$={currentAppId$}
|
||||
/>
|
||||
<RedirectAppLinks {...{ currentAppId, navigateToUrl }}>
|
||||
<ElasticAgentCardComponent {...{ ...props, href, canAccessFleet }} />
|
||||
</RedirectAppLinks>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { getClosestLink, hasActiveModifierKey } from '@kbn/shared-ux-utility';
|
||||
|
||||
interface CreateCrossAppClickHandlerOptions {
|
||||
navigateToUrl(url: string): Promise<void>;
|
||||
container?: HTMLElement;
|
||||
}
|
||||
|
||||
export const createNavigateToUrlClickHandler = ({
|
||||
container,
|
||||
navigateToUrl,
|
||||
}: CreateCrossAppClickHandlerOptions): React.MouseEventHandler<HTMLElement> => {
|
||||
return (e) => {
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
// see https://github.com/DefinitelyTyped/DefinitelyTyped/pull/12239
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
const link = getClosestLink(target, container);
|
||||
if (!link) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isNotEmptyHref = link.href;
|
||||
const hasNoTarget = link.target === '' || link.target === '_self';
|
||||
const isLeftClickOnly = e.button === 0;
|
||||
|
||||
if (
|
||||
isNotEmptyHref &&
|
||||
hasNoTarget &&
|
||||
isLeftClickOnly &&
|
||||
!e.defaultPrevented &&
|
||||
!hasActiveModifierKey(e)
|
||||
) {
|
||||
e.preventDefault();
|
||||
navigateToUrl(link.href);
|
||||
}
|
||||
};
|
||||
};
|
|
@ -1,18 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
/* eslint-disable import/no-default-export */
|
||||
|
||||
import { RedirectAppLinks } from './redirect_app_links';
|
||||
export type { RedirectAppLinksProps } from './redirect_app_links';
|
||||
export { RedirectAppLinks } from './redirect_app_links';
|
||||
|
||||
/**
|
||||
* Exporting the RedirectAppLinks component as a default export so it can be
|
||||
* loaded by React.lazy.
|
||||
*/
|
||||
export default RedirectAppLinks;
|
|
@ -1,12 +0,0 @@
|
|||
---
|
||||
id: sharedUX/Components/AppLink
|
||||
slug: /shared-ux/components/redirect-app-link
|
||||
title: Redirect App Link
|
||||
summary: The component for redirect links.
|
||||
tags: ['shared-ux', 'component']
|
||||
date: 2022-02-01
|
||||
---
|
||||
|
||||
> This documentation is in progress.
|
||||
|
||||
**This component has been refactored.** Instead of requiring the entire `application`, it instead takes just `navigateToUrl` and `currentAppId$`. This makes the component more lightweight.
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* 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 { EuiButton } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { RedirectAppLinks } from './redirect_app_links';
|
||||
import mdx from './redirect_app_links.mdx';
|
||||
|
||||
export default {
|
||||
title: 'Redirect App Links',
|
||||
description: 'app links component that takes in an application id and navigation url.',
|
||||
parameters: {
|
||||
docs: {
|
||||
page: mdx,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
return (
|
||||
<RedirectAppLinks
|
||||
navigateToUrl={() => Promise.resolve()}
|
||||
currentAppId$={new BehaviorSubject('test')}
|
||||
>
|
||||
<EuiButton
|
||||
data-test-subj="storybookButton"
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
onClick={action('button pressed')}
|
||||
>
|
||||
Test link
|
||||
</EuiButton>
|
||||
</RedirectAppLinks>
|
||||
);
|
||||
};
|
|
@ -1,249 +0,0 @@
|
|||
/*
|
||||
* 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 React, { MouseEvent } from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { RedirectAppLinks } from './redirect_app_links';
|
||||
|
||||
export type UnmountCallback = () => void;
|
||||
export type MountPoint<T extends HTMLElement = HTMLElement> = (element: T) => UnmountCallback;
|
||||
|
||||
const createServiceMock = () => {
|
||||
const currentAppId$ = new BehaviorSubject<string>('currentApp');
|
||||
|
||||
return {
|
||||
currentAppId$: currentAppId$.asObservable(),
|
||||
navigateToApp: jest.fn(),
|
||||
navigateToUrl: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
|
||||
describe('RedirectAppLinks', () => {
|
||||
let application = createServiceMock();
|
||||
|
||||
beforeEach(() => {
|
||||
application = createServiceMock();
|
||||
});
|
||||
|
||||
it('intercept click events on children link elements', () => {
|
||||
let event: MouseEvent;
|
||||
const component = mount(
|
||||
<div
|
||||
onClick={(e) => {
|
||||
event = e;
|
||||
}}
|
||||
>
|
||||
<RedirectAppLinks
|
||||
navigateToUrl={application.navigateToUrl}
|
||||
currentAppId$={application.currentAppId$}
|
||||
>
|
||||
<div>
|
||||
<a href="/mocked-anyway">content</a>
|
||||
</div>
|
||||
</RedirectAppLinks>
|
||||
</div>
|
||||
);
|
||||
|
||||
component.find('a').simulate('click', { button: 0, defaultPrevented: false });
|
||||
expect(application.navigateToUrl).toHaveBeenCalledTimes(1);
|
||||
expect(event!.defaultPrevented).toBe(true);
|
||||
});
|
||||
|
||||
it('intercept click events on children inside link elements', async () => {
|
||||
let event: MouseEvent;
|
||||
|
||||
const component = mount(
|
||||
<div
|
||||
onClick={(e) => {
|
||||
event = e;
|
||||
}}
|
||||
>
|
||||
<RedirectAppLinks
|
||||
navigateToUrl={application.navigateToUrl}
|
||||
currentAppId$={application.currentAppId$}
|
||||
>
|
||||
<div>
|
||||
<a href="/mocked-anyway">
|
||||
<span>content</span>
|
||||
</a>
|
||||
</div>
|
||||
</RedirectAppLinks>
|
||||
</div>
|
||||
);
|
||||
|
||||
component.find('span').simulate('click', { button: 0, defaultPrevented: false });
|
||||
|
||||
expect(application.navigateToUrl).toHaveBeenCalledTimes(1);
|
||||
expect(event!.defaultPrevented).toBe(true);
|
||||
});
|
||||
|
||||
it('does not intercept click events when the target is not inside a link', () => {
|
||||
let event: MouseEvent;
|
||||
|
||||
const component = mount(
|
||||
<div
|
||||
onClick={(e) => {
|
||||
event = e;
|
||||
}}
|
||||
>
|
||||
<RedirectAppLinks navigateToUrl={jest.fn()} currentAppId$={application.currentAppId$}>
|
||||
<span>
|
||||
<a href="/mocked-anyway">content</a>
|
||||
</span>
|
||||
</RedirectAppLinks>
|
||||
</div>
|
||||
);
|
||||
|
||||
component.find('span').simulate('click', { button: 0, defaultPrevented: false });
|
||||
|
||||
expect(application.navigateToApp).not.toHaveBeenCalled();
|
||||
expect(event!.defaultPrevented).toBe(false);
|
||||
});
|
||||
|
||||
it('does not intercept click events when the link is a parent of the container', () => {
|
||||
let event: MouseEvent;
|
||||
|
||||
const component = mount(
|
||||
<div
|
||||
onClick={(e) => {
|
||||
event = e;
|
||||
}}
|
||||
>
|
||||
<a href="/mocked-anyway">
|
||||
<RedirectAppLinks navigateToUrl={jest.fn()} currentAppId$={application.currentAppId$}>
|
||||
<span>content</span>
|
||||
</RedirectAppLinks>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
|
||||
component.find('span').simulate('click', { button: 0, defaultPrevented: false });
|
||||
|
||||
expect(application.navigateToApp).not.toHaveBeenCalled();
|
||||
expect(event!.defaultPrevented).toBe(false);
|
||||
});
|
||||
|
||||
it('does not intercept click events when the link has an external target', () => {
|
||||
let event: MouseEvent;
|
||||
|
||||
const component = mount(
|
||||
<div
|
||||
onClick={(e) => {
|
||||
event = e;
|
||||
}}
|
||||
>
|
||||
<RedirectAppLinks navigateToUrl={jest.fn()} currentAppId$={application.currentAppId$}>
|
||||
<a href="/mocked-anyway" target="_blank">
|
||||
content
|
||||
</a>
|
||||
</RedirectAppLinks>
|
||||
</div>
|
||||
);
|
||||
|
||||
component.find('a').simulate('click', { button: 0, defaultPrevented: false });
|
||||
|
||||
expect(application.navigateToApp).not.toHaveBeenCalled();
|
||||
expect(event!.defaultPrevented).toBe(false);
|
||||
});
|
||||
|
||||
it('does not intercept click events when the event is already defaultPrevented', () => {
|
||||
let event: MouseEvent;
|
||||
|
||||
const component = mount(
|
||||
<div
|
||||
onClick={(e) => {
|
||||
event = e;
|
||||
}}
|
||||
>
|
||||
<RedirectAppLinks navigateToUrl={jest.fn()} currentAppId$={application.currentAppId$}>
|
||||
<a href="/mocked-anyway" target="_blank">
|
||||
<span onClick={(e) => e.preventDefault()}>content</span>
|
||||
</a>
|
||||
</RedirectAppLinks>
|
||||
</div>
|
||||
);
|
||||
|
||||
component.find('span').simulate('click', { button: 0, defaultPrevented: false });
|
||||
|
||||
expect(application.navigateToApp).not.toHaveBeenCalled();
|
||||
expect(event!.defaultPrevented).toBe(true);
|
||||
});
|
||||
|
||||
it('does not intercept click events when the event propagation is stopped', () => {
|
||||
let event: MouseEvent;
|
||||
|
||||
const component = mount(
|
||||
<div
|
||||
onClick={(e) => {
|
||||
event = e;
|
||||
}}
|
||||
>
|
||||
<RedirectAppLinks navigateToUrl={jest.fn()} currentAppId$={application.currentAppId$}>
|
||||
<a href="/mocked-anyway" target="_blank" onClick={(e) => e.stopPropagation()}>
|
||||
content
|
||||
</a>
|
||||
</RedirectAppLinks>
|
||||
</div>
|
||||
);
|
||||
|
||||
component.find('a').simulate('click', { button: 0, defaultPrevented: false });
|
||||
|
||||
expect(application.navigateToApp).not.toHaveBeenCalled();
|
||||
expect(event!).toBe(undefined);
|
||||
});
|
||||
|
||||
it('does not intercept click events when the event is not triggered from the left button', () => {
|
||||
let event: MouseEvent;
|
||||
|
||||
const component = mount(
|
||||
<div
|
||||
onClick={(e) => {
|
||||
event = e;
|
||||
}}
|
||||
>
|
||||
<RedirectAppLinks navigateToUrl={jest.fn()} currentAppId$={application.currentAppId$}>
|
||||
<div>
|
||||
<a href="/mocked-anyway">content</a>
|
||||
</div>
|
||||
</RedirectAppLinks>
|
||||
</div>
|
||||
);
|
||||
|
||||
component.find('a').simulate('click', { button: 1, defaultPrevented: false });
|
||||
|
||||
expect(application.navigateToApp).not.toHaveBeenCalled();
|
||||
expect(event!.defaultPrevented).toBe(false);
|
||||
});
|
||||
|
||||
it('does not intercept click events when the event has a modifier key enabled', () => {
|
||||
let event: MouseEvent;
|
||||
|
||||
const component = mount(
|
||||
<div
|
||||
onClick={(e) => {
|
||||
event = e;
|
||||
}}
|
||||
>
|
||||
<RedirectAppLinks navigateToUrl={jest.fn()} currentAppId$={application.currentAppId$}>
|
||||
<div>
|
||||
<a href="/mocked-anyway">content</a>
|
||||
</div>
|
||||
</RedirectAppLinks>
|
||||
</div>
|
||||
);
|
||||
|
||||
component.find('a').simulate('click', { button: 0, ctrlKey: true, defaultPrevented: false });
|
||||
|
||||
expect(application.navigateToApp).not.toHaveBeenCalled();
|
||||
expect(event!.defaultPrevented).toBe(false);
|
||||
});
|
||||
});
|
|
@ -1,68 +0,0 @@
|
|||
/*
|
||||
* 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 React, { useRef, useMemo } from 'react';
|
||||
import type { HTMLAttributes, DetailedHTMLProps, FC } from 'react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { createNavigateToUrlClickHandler } from './click_handler';
|
||||
|
||||
type DivProps = DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
|
||||
/**
|
||||
* TODO: this interface recreates props from the `ApplicationStart` interface.
|
||||
* see: https://github.com/elastic/kibana/issues/127695
|
||||
*/
|
||||
export interface RedirectAppLinksProps extends DivProps {
|
||||
currentAppId$: Observable<string | undefined>;
|
||||
navigateToUrl(url: string): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility component that will intercept click events on children anchor (`<a>`) elements to call
|
||||
* `application.navigateToUrl` with the link's href. This will trigger SPA friendly navigation
|
||||
* when the link points to a valid Kibana app.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <RedirectAppLinks navigateToUrl={() => url} currentAppId$={observableAppId}>
|
||||
* <a href="/base-path/app/another-app/some-path">Go to another-app</a>
|
||||
* </RedirectAppLinks>
|
||||
* ```
|
||||
*
|
||||
* @remarks
|
||||
* It is recommended to use the component at the highest possible level of the component tree that would
|
||||
* require to handle the links. A good practice is to consider it as a context provider and to use it
|
||||
* at the root level of an application or of the page that require the feature.
|
||||
*/
|
||||
export const RedirectAppLinks: FC<RedirectAppLinksProps> = ({
|
||||
navigateToUrl,
|
||||
currentAppId$,
|
||||
children,
|
||||
...otherProps
|
||||
}) => {
|
||||
const currentAppId = useObservable(currentAppId$, undefined);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const clickHandler = useMemo(
|
||||
() =>
|
||||
containerRef.current && currentAppId
|
||||
? createNavigateToUrlClickHandler({
|
||||
container: containerRef.current,
|
||||
navigateToUrl,
|
||||
})
|
||||
: undefined,
|
||||
[currentAppId, navigateToUrl]
|
||||
);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
|
||||
<div ref={containerRef} {...otherProps} onClick={clickHandler}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -4,7 +4,9 @@ exports[`<IconButtonGroup /> is rendered 1`] = `
|
|||
<SharedUxServicesProvider
|
||||
application={
|
||||
Object {
|
||||
"currentAppId$": Observable {},
|
||||
"currentAppId$": Observable {
|
||||
"_subscribe": [Function],
|
||||
},
|
||||
"navigateToUrl": [Function],
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,9 @@ exports[`<ToolbarButton /> is rendered 1`] = `
|
|||
<SharedUxServicesProvider
|
||||
application={
|
||||
Object {
|
||||
"currentAppId$": Observable {},
|
||||
"currentAppId$": Observable {
|
||||
"_subscribe": [Function],
|
||||
},
|
||||
"navigateToUrl": [Function],
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,5 +17,7 @@ export type MockApplicationServiceFactory = ServiceFactory<SharedUxApplicationSe
|
|||
*/
|
||||
export const applicationServiceFactory: MockApplicationServiceFactory = () => ({
|
||||
navigateToUrl: () => Promise.resolve(),
|
||||
currentAppId$: new Observable(),
|
||||
currentAppId$: new Observable((subscriber) => {
|
||||
subscriber.next('abc123');
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -16,8 +16,8 @@ export type ApplicationServiceFactory = ServiceFactory<SharedUxApplicationServic
|
|||
* A factory function for creating for creating a storybook implementation of `SharedUXApplicationService`.
|
||||
*/
|
||||
export const applicationServiceFactory: ApplicationServiceFactory = () => ({
|
||||
navigateToUrl: () => {
|
||||
action('NavigateToUrl');
|
||||
navigateToUrl: (url) => {
|
||||
action('navigateToUrl')(url);
|
||||
return Promise.resolve();
|
||||
},
|
||||
currentAppId$: new BehaviorSubject('123'),
|
||||
|
|
140
packages/shared-ux/link/redirect_app/BUILD.bazel
Normal file
140
packages/shared-ux/link/redirect_app/BUILD.bazel
Normal file
|
@ -0,0 +1,140 @@
|
|||
load("@npm//@bazel/typescript:index.bzl", "ts_config")
|
||||
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
|
||||
load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project")
|
||||
|
||||
PKG_DIRNAME = "redirect_app"
|
||||
PKG_REQUIRE_NAME = "@kbn/shared-ux-link-redirect-app"
|
||||
|
||||
SOURCE_FILES = glob(
|
||||
[
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.mdx",
|
||||
],
|
||||
exclude = [
|
||||
"**/*.test.*",
|
||||
],
|
||||
)
|
||||
|
||||
SRCS = SOURCE_FILES
|
||||
|
||||
filegroup(
|
||||
name = "srcs",
|
||||
srcs = SRCS,
|
||||
)
|
||||
|
||||
NPM_MODULE_EXTRA_FILES = [
|
||||
"package.json",
|
||||
]
|
||||
|
||||
# In this array place runtime dependencies, including other packages and NPM packages
|
||||
# which must be available for this code to run.
|
||||
#
|
||||
# To reference other packages use:
|
||||
# "//repo/relative/path/to/package"
|
||||
# eg. "//packages/kbn-utils"
|
||||
#
|
||||
# To reference a NPM package use:
|
||||
# "@npm//name-of-package"
|
||||
# eg. "@npm//lodash"
|
||||
RUNTIME_DEPS = [
|
||||
"@npm//@elastic/eui",
|
||||
"@npm//@storybook/addon-actions",
|
||||
"@npm//react-use",
|
||||
"@npm//react",
|
||||
"@npm//rxjs",
|
||||
"//packages/kbn-shared-ux-utility",
|
||||
]
|
||||
|
||||
# In this array place dependencies necessary to build the types, which will include the
|
||||
# :npm_module_types target of other packages and packages from NPM, including @types/*
|
||||
# packages.
|
||||
#
|
||||
# To reference the types for another package use:
|
||||
# "//repo/relative/path/to/package:npm_module_types"
|
||||
# eg. "//packages/kbn-utils:npm_module_types"
|
||||
#
|
||||
# References to NPM packages work the same as RUNTIME_DEPS
|
||||
TYPES_DEPS = [
|
||||
"@npm//@elastic/eui",
|
||||
"@npm//@storybook/addon-actions",
|
||||
"@npm//@types/jest",
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/react",
|
||||
"@npm//rxjs",
|
||||
"@npm//react-use",
|
||||
"//packages/kbn-ambient-ui-types",
|
||||
"//packages/kbn-shared-ux-utility:npm_module_types",
|
||||
]
|
||||
|
||||
jsts_transpiler(
|
||||
name = "target_node",
|
||||
srcs = SRCS,
|
||||
build_pkg_name = package_name(),
|
||||
)
|
||||
|
||||
jsts_transpiler(
|
||||
name = "target_web",
|
||||
srcs = SRCS,
|
||||
build_pkg_name = package_name(),
|
||||
web = True,
|
||||
additional_args = [
|
||||
"--copy-files",
|
||||
"--quiet"
|
||||
],
|
||||
)
|
||||
|
||||
ts_config(
|
||||
name = "tsconfig",
|
||||
src = "tsconfig.json",
|
||||
deps = [
|
||||
"//:tsconfig.base.json",
|
||||
"//:tsconfig.bazel.json",
|
||||
],
|
||||
)
|
||||
|
||||
ts_project(
|
||||
name = "tsc_types",
|
||||
args = ['--pretty'],
|
||||
srcs = SRCS,
|
||||
deps = TYPES_DEPS,
|
||||
declaration = True,
|
||||
emit_declaration_only = True,
|
||||
out_dir = "target_types",
|
||||
root_dir = "src",
|
||||
tsconfig = ":tsconfig",
|
||||
)
|
||||
|
||||
js_library(
|
||||
name = PKG_DIRNAME,
|
||||
srcs = NPM_MODULE_EXTRA_FILES,
|
||||
deps = RUNTIME_DEPS + [":target_node", ":target_web"],
|
||||
package_name = PKG_REQUIRE_NAME,
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
pkg_npm(
|
||||
name = "npm_module",
|
||||
deps = [":" + PKG_DIRNAME],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "build",
|
||||
srcs = [":npm_module"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
pkg_npm_types(
|
||||
name = "npm_module_types",
|
||||
srcs = SRCS,
|
||||
deps = [":tsc_types"],
|
||||
package_name = PKG_REQUIRE_NAME,
|
||||
tsconfig = ":tsconfig",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "build_types",
|
||||
srcs = [":npm_module_types"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
86
packages/shared-ux/link/redirect_app/README.mdx
Normal file
86
packages/shared-ux/link/redirect_app/README.mdx
Normal file
|
@ -0,0 +1,86 @@
|
|||
---
|
||||
id: sharedUX/Components/AppLink
|
||||
slug: /shared-ux/components/redirect-app-links
|
||||
title: Redirect App Links
|
||||
summary: A component for redirecting links contained within it to the appropriate Kibana solution without a page refresh.
|
||||
tags: ['shared-ux', 'component']
|
||||
date: 2022-05-04
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
This component is an "area of effect" component, which produces a container that intercepts actions for specific elements within it. In this case, the container intercepts clicks on anchor elements and redirects them to Kibana solutions without a page refresh.
|
||||
|
||||
## Pure Component
|
||||
|
||||
The pure component allows you create a container to intercept clicks without contextual services, (e.g. Kibana Core). This likely does not have much utility for solutions in Kibana, but rather is useful for shared components where we want to ensure clicks are redirected correctly.
|
||||
|
||||
```tsx
|
||||
import { RedirectAppLinksComponent as RedirectAppLinks } from '@kbn/shared-ux-links-redirect-app';
|
||||
|
||||
<RedirectAppLinks currentAppId="appId" navigateToUrl={(url) => { ... }}>
|
||||
<a href="/base-path/app/another-app/some-path">Go to another-app</a>
|
||||
</RedirectAppLinks>
|
||||
```
|
||||
|
||||
## Connected Component
|
||||
|
||||
The connected component uses a React Context to access services that provide the current app id and a function to navigate to a new url. This is useful in that a solution can wrap their entire application in the context and use `RedirectAppLinks` in specific areas.
|
||||
|
||||
```tsx
|
||||
import { RedirectAppLinksContainer as RedirectAppLinks, RedirectAppLinksProvider } from '@kbn/shared-ux-links-redirect-app';
|
||||
|
||||
<RedirectAppLinksProvider currentAppId="appId" navigateToUrl={(url) => { ... }}>
|
||||
.
|
||||
{/* other components that don't need to redirect */}
|
||||
.
|
||||
<RedirectAppLinks>
|
||||
<a href="/base-path/app/another-app/some-path">Go to another-app</a>
|
||||
</RedirectAppLinks>
|
||||
.
|
||||
.
|
||||
.
|
||||
</RedirectAppLinksProvider>
|
||||
```
|
||||
|
||||
You can also use the Kibana provider:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
RedirectAppLinksContainer as RedirectAppLinks,
|
||||
RedirectAppLinksKibanaProvider as RedirectAppLinksProvider
|
||||
} from '@kbn/shared-ux-links-redirect-app';
|
||||
|
||||
<RedirectAppLinksProvider {...coreStart}>
|
||||
.
|
||||
{/* other components that don't need to redirect */}
|
||||
.
|
||||
<RedirectAppLinks>
|
||||
<a href="/base-path/app/another-app/some-path">Go to another-app</a>
|
||||
</RedirectAppLinks>
|
||||
.
|
||||
.
|
||||
</RedirectAppLinksProvider>
|
||||
```
|
||||
|
||||
## Top-level Component
|
||||
|
||||
This is the component is likely the most useful to solutions in Kibana. It assumes an entire solution needs this redirect functionality, and combines the context provider with the container. This top-level component can be used with either pure props or with Kibana services.
|
||||
|
||||
```tsx
|
||||
import { RedirectAppLinks } from '@kbn/shared-ux-links-redirect-app';
|
||||
|
||||
<RedirectAppLinksProvider currentAppId="appId" navigateToUrl={(url) => { ... }}>
|
||||
.
|
||||
<a href="/base-path/app/another-app/some-path">Go to another-app</a>
|
||||
.
|
||||
</RedirectAppLinksProvider>
|
||||
|
||||
{/* OR */}
|
||||
|
||||
<RedirectAppLinksProvider {...coreStart}>
|
||||
.
|
||||
<a href="/base-path/app/another-app/some-path">Go to another-app</a>
|
||||
.
|
||||
</RedirectAppLinksProvider>
|
||||
```
|
14
packages/shared-ux/link/redirect_app/jest.config.js
Normal file
14
packages/shared-ux/link/redirect_app/jest.config.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../../..',
|
||||
roots: ['<rootDir>/packages/shared-ux/link/redirect_app'],
|
||||
verbose: true,
|
||||
};
|
8
packages/shared-ux/link/redirect_app/package.json
Normal file
8
packages/shared-ux/link/redirect_app/package.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "@kbn/shared-ux-link-redirect-app",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"main": "./target_node/index.js",
|
||||
"browser": "./target_web/index.js",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||
}
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { MouseEvent } from 'react';
|
||||
import { createNavigateToUrlClickHandler } from './click_handler';
|
||||
import { navigateToUrlClickHandler } from './click_handler';
|
||||
|
||||
const createLink = ({
|
||||
href = '/base-path/app/targetApp',
|
||||
|
@ -43,27 +43,59 @@ const createEvent = ({
|
|||
|
||||
type NavigateToURLFn = (url: string) => Promise<void>;
|
||||
|
||||
describe('createNavigateToUrlClickHandler', () => {
|
||||
describe('navigateToUrlClickHandler', () => {
|
||||
let container: HTMLElement;
|
||||
let navigateToUrl: jest.MockedFunction<NavigateToURLFn>;
|
||||
const currentAppId = 'abc123';
|
||||
|
||||
const createHandler = () =>
|
||||
createNavigateToUrlClickHandler({
|
||||
const handler = (event: MouseEvent<HTMLElement>): void => {
|
||||
navigateToUrlClickHandler({
|
||||
event,
|
||||
currentAppId,
|
||||
container,
|
||||
navigateToUrl,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
navigateToUrl = jest.fn();
|
||||
});
|
||||
|
||||
it('calls `navigateToUrl` with the link url', () => {
|
||||
const handler = createHandler();
|
||||
|
||||
it("doesn't call `navigateToUrl` without a container", () => {
|
||||
const event = createEvent({
|
||||
target: createLink({ href: '/base-path/app/targetApp' }),
|
||||
});
|
||||
|
||||
navigateToUrlClickHandler({
|
||||
event,
|
||||
currentAppId,
|
||||
container: null,
|
||||
navigateToUrl,
|
||||
});
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it("doesn't call `navigateToUrl` without a `currentAppId`", () => {
|
||||
const event = createEvent({
|
||||
target: createLink({ href: '/base-path/app/targetApp' }),
|
||||
});
|
||||
|
||||
navigateToUrlClickHandler({
|
||||
event,
|
||||
container,
|
||||
navigateToUrl,
|
||||
});
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('calls `navigateToUrl` with the link url', () => {
|
||||
const event = createEvent({
|
||||
target: createLink({ href: '/base-path/app/targetApp' }),
|
||||
});
|
||||
|
||||
handler(event);
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalledTimes(1);
|
||||
|
@ -71,13 +103,12 @@ describe('createNavigateToUrlClickHandler', () => {
|
|||
});
|
||||
|
||||
it('is triggered if a non-link target has a parent link', () => {
|
||||
const handler = createHandler();
|
||||
|
||||
const link = createLink();
|
||||
const target = document.createElement('span');
|
||||
link.appendChild(target);
|
||||
|
||||
const event = createEvent({ target });
|
||||
|
||||
handler(event);
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalledTimes(1);
|
||||
|
@ -85,13 +116,12 @@ describe('createNavigateToUrlClickHandler', () => {
|
|||
});
|
||||
|
||||
it('is not triggered if a non-link target has no parent link', () => {
|
||||
const handler = createHandler();
|
||||
|
||||
const parent = document.createElement('div');
|
||||
const target = document.createElement('span');
|
||||
parent.appendChild(target);
|
||||
|
||||
const event = createEvent({ target });
|
||||
|
||||
handler(event);
|
||||
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
|
@ -99,11 +129,10 @@ describe('createNavigateToUrlClickHandler', () => {
|
|||
});
|
||||
|
||||
it('is not triggered when the link has no href', () => {
|
||||
const handler = createHandler();
|
||||
|
||||
const event = createEvent({
|
||||
target: createLink({ href: '' }),
|
||||
});
|
||||
|
||||
handler(event);
|
||||
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
|
@ -111,11 +140,10 @@ describe('createNavigateToUrlClickHandler', () => {
|
|||
});
|
||||
|
||||
it('is only triggered when the link does not have an external target', () => {
|
||||
const handler = createHandler();
|
||||
|
||||
let event = createEvent({
|
||||
target: createLink({ target: '_blank' }),
|
||||
});
|
||||
|
||||
handler(event);
|
||||
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
|
@ -124,6 +152,7 @@ describe('createNavigateToUrlClickHandler', () => {
|
|||
event = createEvent({
|
||||
target: createLink({ target: 'some-target' }),
|
||||
});
|
||||
|
||||
handler(event);
|
||||
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
|
@ -132,6 +161,7 @@ describe('createNavigateToUrlClickHandler', () => {
|
|||
event = createEvent({
|
||||
target: createLink({ target: '_self' }),
|
||||
});
|
||||
|
||||
handler(event);
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalledTimes(1);
|
||||
|
@ -140,6 +170,7 @@ describe('createNavigateToUrlClickHandler', () => {
|
|||
event = createEvent({
|
||||
target: createLink({ target: '' }),
|
||||
});
|
||||
|
||||
handler(event);
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalledTimes(1);
|
||||
|
@ -147,11 +178,10 @@ describe('createNavigateToUrlClickHandler', () => {
|
|||
});
|
||||
|
||||
it('is only triggered from left clicks', () => {
|
||||
const handler = createHandler();
|
||||
|
||||
let event = createEvent({
|
||||
button: 1,
|
||||
});
|
||||
|
||||
handler(event);
|
||||
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
|
@ -160,6 +190,7 @@ describe('createNavigateToUrlClickHandler', () => {
|
|||
event = createEvent({
|
||||
button: 12,
|
||||
});
|
||||
|
||||
handler(event);
|
||||
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
|
@ -168,6 +199,7 @@ describe('createNavigateToUrlClickHandler', () => {
|
|||
event = createEvent({
|
||||
button: 0,
|
||||
});
|
||||
|
||||
handler(event);
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalledTimes(1);
|
||||
|
@ -175,11 +207,10 @@ describe('createNavigateToUrlClickHandler', () => {
|
|||
});
|
||||
|
||||
it('is not triggered if the event default is prevented', () => {
|
||||
const handler = createHandler();
|
||||
|
||||
let event = createEvent({
|
||||
defaultPrevented: true,
|
||||
});
|
||||
|
||||
handler(event);
|
||||
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
|
@ -188,6 +219,7 @@ describe('createNavigateToUrlClickHandler', () => {
|
|||
event = createEvent({
|
||||
defaultPrevented: false,
|
||||
});
|
||||
|
||||
handler(event);
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalledTimes(1);
|
||||
|
@ -195,15 +227,15 @@ describe('createNavigateToUrlClickHandler', () => {
|
|||
});
|
||||
|
||||
it('is not triggered if any modifier key is pressed', () => {
|
||||
const handler = createHandler();
|
||||
|
||||
let event = createEvent({ modifierKey: true });
|
||||
|
||||
handler(event);
|
||||
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
expect(navigateToUrl).not.toHaveBeenCalled();
|
||||
|
||||
event = createEvent({ modifierKey: false });
|
||||
|
||||
handler(event);
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalledTimes(1);
|
57
packages/shared-ux/link/redirect_app/src/click_handler.ts
Normal file
57
packages/shared-ux/link/redirect_app/src/click_handler.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 { MouseEvent } from 'react';
|
||||
import { getClosestLink, hasActiveModifierKey } from '@kbn/shared-ux-utility';
|
||||
import { NavigateToUrl } from './types';
|
||||
|
||||
interface CreateCrossAppClickHandlerOptions {
|
||||
event: MouseEvent<HTMLElement>;
|
||||
navigateToUrl: NavigateToUrl;
|
||||
container: HTMLElement | null;
|
||||
currentAppId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a click handler that will redirect the user using `navigateToUrl` if the
|
||||
* correct conditions are met.
|
||||
*/
|
||||
export const navigateToUrlClickHandler = ({
|
||||
event,
|
||||
container,
|
||||
navigateToUrl,
|
||||
currentAppId,
|
||||
}: CreateCrossAppClickHandlerOptions) => {
|
||||
if (!container || !currentAppId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// see https://github.com/DefinitelyTyped/DefinitelyTyped/pull/12239
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
const link = getClosestLink(target, container);
|
||||
|
||||
if (!link) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isNotEmptyHref = link.href;
|
||||
const hasNoTarget = link.target === '' || link.target === '_self';
|
||||
const isLeftClickOnly = event.button === 0;
|
||||
|
||||
if (
|
||||
isNotEmptyHref &&
|
||||
hasNoTarget &&
|
||||
isLeftClickOnly &&
|
||||
!event.defaultPrevented &&
|
||||
!hasActiveModifierKey(event)
|
||||
) {
|
||||
event.preventDefault();
|
||||
navigateToUrl(link.href);
|
||||
}
|
||||
};
|
39
packages/shared-ux/link/redirect_app/src/index.tsx
Normal file
39
packages/shared-ux/link/redirect_app/src/index.tsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { RedirectAppLinks as RedirectAppLinksContainer } from './redirect_app_links';
|
||||
export { RedirectAppLinks as RedirectAppLinksComponent } from './redirect_app_links';
|
||||
export { RedirectAppLinksKibanaProvider, RedirectAppLinksProvider } from './services';
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import { RedirectAppLinks as RedirectAppLinksContainer } from './redirect_app_links';
|
||||
import {
|
||||
Services,
|
||||
KibanaServices,
|
||||
RedirectAppLinksKibanaProvider,
|
||||
RedirectAppLinksProvider,
|
||||
} from './services';
|
||||
|
||||
const isKibanaContract = (services: any): services is KibanaServices => {
|
||||
return typeof services.coreStart !== 'undefined';
|
||||
};
|
||||
|
||||
/**
|
||||
* This component composes `RedirectAppLinksContainer` with either `RedirectAppLinksProvider` or
|
||||
* `RedirectAppLinksKibanaProvider` based on the services provided, creating a single component
|
||||
* with which consumers can wrap their components or solutions.
|
||||
*/
|
||||
export const RedirectAppLinks: FC<Services | KibanaServices> = ({ children, ...services }) => {
|
||||
const container = <RedirectAppLinksContainer>{children}</RedirectAppLinksContainer>;
|
||||
|
||||
return isKibanaContract(services) ? (
|
||||
<RedirectAppLinksKibanaProvider {...services}>{container}</RedirectAppLinksKibanaProvider>
|
||||
) : (
|
||||
<RedirectAppLinksProvider {...services}>{container}</RedirectAppLinksProvider>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 React, { useRef, MouseEventHandler, useCallback } from 'react';
|
||||
import type { HTMLAttributes, DetailedHTMLProps, FC } from 'react';
|
||||
|
||||
import { navigateToUrlClickHandler } from './click_handler';
|
||||
import { NavigateToUrl } from './types';
|
||||
|
||||
export interface Props extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
|
||||
navigateToUrl: NavigateToUrl;
|
||||
currentAppId?: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility component that will intercept click events on children anchor (`<a>`) elements to call
|
||||
* `navigateToUrl` with the link's href. This will trigger SPA friendly navigation when the link points
|
||||
* to a valid Kibana app.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <RedirectAppLinks currentAppId="appId" navigateToUrl={(url) => { ... }}>
|
||||
* <a href="/base-path/app/another-app/some-path">Go to another-app</a>
|
||||
* </RedirectAppLinks>
|
||||
* ```
|
||||
*/
|
||||
export const RedirectAppLinks: FC<Props> = ({
|
||||
children,
|
||||
navigateToUrl,
|
||||
currentAppId,
|
||||
...otherProps
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleClick: MouseEventHandler<HTMLDivElement> = useCallback(
|
||||
(event) =>
|
||||
navigateToUrlClickHandler({
|
||||
event,
|
||||
currentAppId,
|
||||
navigateToUrl,
|
||||
container: containerRef.current,
|
||||
}),
|
||||
[currentAppId, navigateToUrl]
|
||||
);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
|
||||
<div {...otherProps} ref={containerRef} onClick={handleClick}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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 { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { RedirectAppLinks } from '.';
|
||||
import mdx from '../README.mdx';
|
||||
|
||||
export default {
|
||||
title: 'Redirect App Links',
|
||||
description:
|
||||
'An "area of effect" component which intercepts clicks on anchor elements and redirects them to Kibana solutions without a page refresh.',
|
||||
parameters: {
|
||||
docs: {
|
||||
page: mdx,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
const navigateToUrl = async (url: string) => {
|
||||
action('navigateToUrl')(url);
|
||||
};
|
||||
|
||||
const currentAppId = 'abc123';
|
||||
|
||||
return (
|
||||
<>
|
||||
<RedirectAppLinks {...{ currentAppId, navigateToUrl }}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="storybookButton"
|
||||
iconType="plusInCircle"
|
||||
href="/some-test-url"
|
||||
>
|
||||
Button with URL
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="storybookButton"
|
||||
iconType="plusInCircle"
|
||||
onClick={action('onClick')}
|
||||
>
|
||||
Button without URL
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</RedirectAppLinks>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="storybookButton"
|
||||
iconType="plusInCircle"
|
||||
href="/?path=/story/redirect-app-links--component"
|
||||
>
|
||||
Button outside RedirectAppLinks
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,292 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import React, { MouseEvent } from 'react';
|
||||
import { mount as enzymeMount, ReactWrapper } from 'enzyme';
|
||||
|
||||
import { RedirectAppLinksKibanaProvider, RedirectAppLinksProvider } from './services';
|
||||
import { RedirectAppLinks } from './redirect_app_links';
|
||||
import { RedirectAppLinks as ComposedWrapper } from '.';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export type UnmountCallback = () => void;
|
||||
export type MountPoint<T extends HTMLElement = HTMLElement> = (element: T) => UnmountCallback;
|
||||
type Mount = (
|
||||
node: React.ReactElement
|
||||
) => ReactWrapper<any, Readonly<{}>, React.Component<{}, {}, any>>;
|
||||
|
||||
const commonTests = (name: string, mount: Mount, navigateToUrl: jest.Mock) => {
|
||||
beforeEach(() => {
|
||||
navigateToUrl.mockReset();
|
||||
});
|
||||
|
||||
describe(`RedirectAppLinks with ${name}`, () => {
|
||||
it('intercept click events on children link elements', () => {
|
||||
let event: MouseEvent;
|
||||
const component = mount(
|
||||
<div
|
||||
onClick={(e) => {
|
||||
event = e;
|
||||
}}
|
||||
>
|
||||
<RedirectAppLinks>
|
||||
<div>
|
||||
<a href="/mocked-anyway">content</a>
|
||||
</div>
|
||||
</RedirectAppLinks>
|
||||
</div>
|
||||
);
|
||||
|
||||
component.find('a').simulate('click', { button: 0, defaultPrevented: false });
|
||||
expect(navigateToUrl).toHaveBeenCalledTimes(1);
|
||||
expect(event!.defaultPrevented).toBe(true);
|
||||
});
|
||||
|
||||
it('intercept click events on children inside link elements', async () => {
|
||||
let event: MouseEvent;
|
||||
|
||||
const component = mount(
|
||||
<div
|
||||
onClick={(e) => {
|
||||
event = e;
|
||||
}}
|
||||
>
|
||||
<RedirectAppLinks>
|
||||
<div>
|
||||
<a href="/mocked-anyway">
|
||||
<span>content</span>
|
||||
</a>
|
||||
</div>
|
||||
</RedirectAppLinks>
|
||||
</div>
|
||||
);
|
||||
|
||||
component.find('span').simulate('click', { button: 0, defaultPrevented: false });
|
||||
|
||||
expect(navigateToUrl).toHaveBeenCalledTimes(1);
|
||||
expect(event!.defaultPrevented).toBe(true);
|
||||
});
|
||||
|
||||
it('does not intercept click events when the target is not inside a link', () => {
|
||||
let event: MouseEvent;
|
||||
|
||||
const component = mount(
|
||||
<div
|
||||
onClick={(e) => {
|
||||
event = e;
|
||||
}}
|
||||
>
|
||||
<RedirectAppLinks>
|
||||
<span>
|
||||
<a href="/mocked-anyway">content</a>
|
||||
</span>
|
||||
</RedirectAppLinks>
|
||||
</div>
|
||||
);
|
||||
|
||||
component.find('span').simulate('click', { button: 0, defaultPrevented: false });
|
||||
|
||||
expect(navigateToUrl).not.toHaveBeenCalled();
|
||||
expect(event!.defaultPrevented).toBe(false);
|
||||
});
|
||||
|
||||
it('does not intercept click events when the link has an external target', () => {
|
||||
let event: MouseEvent;
|
||||
|
||||
const component = mount(
|
||||
<div
|
||||
onClick={(e) => {
|
||||
event = e;
|
||||
}}
|
||||
>
|
||||
<RedirectAppLinks>
|
||||
<a href="/mocked-anyway" target="_blank">
|
||||
content
|
||||
</a>
|
||||
</RedirectAppLinks>
|
||||
</div>
|
||||
);
|
||||
|
||||
component.find('a').simulate('click', { button: 0, defaultPrevented: false });
|
||||
|
||||
expect(navigateToUrl).not.toHaveBeenCalled();
|
||||
expect(event!.defaultPrevented).toBe(false);
|
||||
});
|
||||
|
||||
it('does not intercept click events when the event is already defaultPrevented', () => {
|
||||
let event: MouseEvent;
|
||||
|
||||
const component = mount(
|
||||
<div
|
||||
onClick={(e) => {
|
||||
event = e;
|
||||
}}
|
||||
>
|
||||
<RedirectAppLinks>
|
||||
<a href="/mocked-anyway" target="_blank">
|
||||
<span onClick={(e) => e.preventDefault()}>content</span>
|
||||
</a>
|
||||
</RedirectAppLinks>
|
||||
</div>
|
||||
);
|
||||
|
||||
component.find('span').simulate('click', { button: 0, defaultPrevented: false });
|
||||
|
||||
expect(navigateToUrl).not.toHaveBeenCalled();
|
||||
expect(event!.defaultPrevented).toBe(true);
|
||||
});
|
||||
|
||||
it('does not intercept click events when the event propagation is stopped', () => {
|
||||
let event: MouseEvent;
|
||||
|
||||
const component = mount(
|
||||
<div
|
||||
onClick={(e) => {
|
||||
event = e;
|
||||
}}
|
||||
>
|
||||
<RedirectAppLinks>
|
||||
<a href="/mocked-anyway" target="_blank" onClick={(e) => e.stopPropagation()}>
|
||||
content
|
||||
</a>
|
||||
</RedirectAppLinks>
|
||||
</div>
|
||||
);
|
||||
|
||||
component.find('a').simulate('click', { button: 0, defaultPrevented: false });
|
||||
|
||||
expect(navigateToUrl).not.toHaveBeenCalled();
|
||||
expect(event!).toBe(undefined);
|
||||
});
|
||||
|
||||
it('does not intercept click events when the event is not triggered from the left button', () => {
|
||||
let event: MouseEvent;
|
||||
|
||||
const component = mount(
|
||||
<div
|
||||
onClick={(e) => {
|
||||
event = e;
|
||||
}}
|
||||
>
|
||||
<RedirectAppLinks>
|
||||
<div>
|
||||
<a href="/mocked-anyway">content</a>
|
||||
</div>
|
||||
</RedirectAppLinks>
|
||||
</div>
|
||||
);
|
||||
|
||||
component.find('a').simulate('click', { button: 1, defaultPrevented: false });
|
||||
|
||||
expect(navigateToUrl).not.toHaveBeenCalled();
|
||||
expect(event!.defaultPrevented).toBe(false);
|
||||
});
|
||||
|
||||
it('does not intercept click events when the event has a modifier key enabled', () => {
|
||||
let event: MouseEvent;
|
||||
|
||||
const component = mount(
|
||||
<div
|
||||
onClick={(e) => {
|
||||
event = e;
|
||||
}}
|
||||
>
|
||||
<RedirectAppLinks>
|
||||
<div>
|
||||
<a href="/mocked-anyway">content</a>
|
||||
</div>
|
||||
</RedirectAppLinks>
|
||||
</div>
|
||||
);
|
||||
|
||||
component.find('a').simulate('click', { button: 0, ctrlKey: true, defaultPrevented: false });
|
||||
|
||||
expect(navigateToUrl).not.toHaveBeenCalled();
|
||||
expect(event!.defaultPrevented).toBe(false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const targetedTests = (name: string, mount: Mount, navigateToUrl: jest.Mock) => {
|
||||
beforeEach(() => {
|
||||
navigateToUrl.mockReset();
|
||||
});
|
||||
|
||||
describe(`${name} with isolated areas of effect`, () => {
|
||||
it(`does not intercept click events when the link is a parent of the container`, () => {
|
||||
let event: MouseEvent;
|
||||
|
||||
const component = mount(
|
||||
<div
|
||||
onClick={(e) => {
|
||||
event = e;
|
||||
}}
|
||||
>
|
||||
<a href="/mocked-anyway">
|
||||
<RedirectAppLinks>
|
||||
<span>content</span>
|
||||
</RedirectAppLinks>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
|
||||
component.find('span').simulate('click', { button: 0, defaultPrevented: false });
|
||||
|
||||
expect(navigateToUrl).not.toHaveBeenCalled();
|
||||
expect(event!.defaultPrevented).toBe(false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
describe('RedirectAppLinks', () => {
|
||||
const navigateToUrl = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
navigateToUrl.mockReset();
|
||||
});
|
||||
|
||||
const kibana = {
|
||||
coreStart: {
|
||||
application: {
|
||||
currentAppId$: new Observable<string>((subscriber) => {
|
||||
subscriber.next('123');
|
||||
}),
|
||||
navigateToUrl,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const services = {
|
||||
currentAppId: 'abc123',
|
||||
navigateToUrl,
|
||||
};
|
||||
|
||||
const provider = (node: React.ReactElement) =>
|
||||
enzymeMount(<RedirectAppLinksProvider {...services}>{node}</RedirectAppLinksProvider>);
|
||||
|
||||
const kibanaProvider = (node: React.ReactElement) =>
|
||||
enzymeMount(
|
||||
<RedirectAppLinksKibanaProvider {...kibana}>{node}</RedirectAppLinksKibanaProvider>
|
||||
);
|
||||
|
||||
const composedProvider = (node: React.ReactElement) =>
|
||||
enzymeMount(<ComposedWrapper {...services}>{node}</ComposedWrapper>);
|
||||
|
||||
const composedKibanaProvider = (node: React.ReactElement) =>
|
||||
enzymeMount(<ComposedWrapper {...kibana}>{node}</ComposedWrapper>);
|
||||
|
||||
describe('Test all Providers', () => {
|
||||
commonTests('RedirectAppLinksProvider', provider, navigateToUrl);
|
||||
targetedTests('RedirectAppLinksProvider', provider, navigateToUrl);
|
||||
commonTests('RedirectAppLinksKibanaProvider', kibanaProvider, navigateToUrl);
|
||||
targetedTests('RedirectAppLinksKibanaProvider', kibanaProvider, navigateToUrl);
|
||||
commonTests('Provider Props', composedProvider, navigateToUrl);
|
||||
commonTests('Kibana Props', composedKibanaProvider, navigateToUrl);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
|
||||
import { useServices } from './services';
|
||||
import {
|
||||
RedirectAppLinks as Component,
|
||||
Props as ComponentProps,
|
||||
} from './redirect_app_links.component';
|
||||
|
||||
type Props = Omit<ComponentProps, 'navigateToUrl' | 'currentAppId'>;
|
||||
|
||||
/**
|
||||
* A service-enabled component that provides Kibana-specific functionality to the `RedirectAppLinks`
|
||||
* pure component.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <RedirectAppLinks>
|
||||
* <a href="/base-path/app/another-app/some-path">Go to another-app</a>
|
||||
* </RedirectAppLinks>
|
||||
* ```
|
||||
*/
|
||||
export const RedirectAppLinks = (props: Props) => <Component {...useServices()} {...props} />;
|
79
packages/shared-ux/link/redirect_app/src/services.tsx
Normal file
79
packages/shared-ux/link/redirect_app/src/services.tsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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 React, { FC, useContext } from 'react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { Observable } from 'rxjs';
|
||||
import { NavigateToUrl } from './types';
|
||||
|
||||
/**
|
||||
* Contextual services for this component.
|
||||
*/
|
||||
export interface Services {
|
||||
navigateToUrl: NavigateToUrl;
|
||||
currentAppId?: string;
|
||||
}
|
||||
|
||||
const RedirectAppLinksContext = React.createContext<Services | null>(null);
|
||||
|
||||
/**
|
||||
* Contextual services Provider.
|
||||
*/
|
||||
export const RedirectAppLinksProvider: FC<Services> = ({ children, ...services }) => {
|
||||
return (
|
||||
<RedirectAppLinksContext.Provider value={{ ...services }}>
|
||||
{children}
|
||||
</RedirectAppLinksContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Kibana-specific contextual services to be adapted for this component.
|
||||
*/
|
||||
export interface KibanaServices {
|
||||
coreStart: {
|
||||
application: {
|
||||
currentAppId$: Observable<string | undefined>;
|
||||
navigateToUrl: NavigateToUrl;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Kibana-specific contextual services Provider.
|
||||
*/
|
||||
export const RedirectAppLinksKibanaProvider: FC<KibanaServices> = ({ children, coreStart }) => {
|
||||
const { navigateToUrl, currentAppId$ } = coreStart.application;
|
||||
const currentAppId = useObservable(currentAppId$, undefined);
|
||||
|
||||
return (
|
||||
<RedirectAppLinksContext.Provider
|
||||
value={{
|
||||
navigateToUrl,
|
||||
currentAppId,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RedirectAppLinksContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* React hook for accessing pre-wired services.
|
||||
*/
|
||||
export function useServices() {
|
||||
const context = useContext(RedirectAppLinksContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'RedirectAppLinksContext is missing. Ensure your component or React root is wrapped with RedirectAppLinksProvider.'
|
||||
);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
9
packages/shared-ux/link/redirect_app/src/types.ts
Normal file
9
packages/shared-ux/link/redirect_app/src/types.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type NavigateToUrl = (url: string) => Promise<void> | void;
|
19
packages/shared-ux/link/redirect_app/tsconfig.json
Normal file
19
packages/shared-ux/link/redirect_app/tsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.bazel.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "target_types",
|
||||
"rootDir": "src",
|
||||
"stripInternal": false,
|
||||
"types": [
|
||||
"jest",
|
||||
"node",
|
||||
"react",
|
||||
"@kbn/ambient-ui-types"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
|
@ -3212,6 +3212,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/shared-ux-link-redirect-app@link:bazel-bin/packages/shared-ux/link/redirect_app":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/shared-ux-services@link:bazel-bin/packages/kbn-shared-ux-services":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
@ -6300,6 +6304,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@types/kbn__shared-ux-link-redirect-app@link:bazel-bin/packages/shared-ux/link/redirect_app/npm_module_types":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@types/kbn__shared-ux-services@link:bazel-bin/packages/kbn-shared-ux-services/npm_module_types":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue