diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index 003dbe3fd22f..51a64ba204a9 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -124,6 +124,7 @@ enabled: - test/functional/apps/kibana_overview/config.ts - test/functional/apps/management/config.ts - test/functional/apps/saved_objects_management/config.ts + - test/functional/apps/sharing/config.ts - test/functional/apps/status_page/config.ts - test/functional/apps/visualize/group1/config.ts - test/functional/apps/visualize/group2/config.ts diff --git a/src/plugins/share/public/lib/get_home_href.ts b/src/plugins/share/public/lib/get_home_href.ts new file mode 100644 index 000000000000..6c2cc4e69529 --- /dev/null +++ b/src/plugins/share/public/lib/get_home_href.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { HttpSetup, IUiSettingsClient } from '@kbn/core/public'; + +export function getHomeHref(http: HttpSetup, uiSettings: IUiSettingsClient) { + return http.basePath.prepend(uiSettings.get('defaultRoute')); +} diff --git a/src/plugins/share/public/url_service/redirect/components/empty_prompt.tsx b/src/plugins/share/public/url_service/redirect/components/empty_prompt.tsx new file mode 100644 index 000000000000..a2ba5a26fede --- /dev/null +++ b/src/plugins/share/public/url_service/redirect/components/empty_prompt.tsx @@ -0,0 +1,68 @@ +/* + * 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 * as React from 'react'; + +import { EuiButtonEmpty } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { ChromeDocTitle } from '@kbn/core-chrome-browser'; +import { NotFoundPrompt } from '@kbn/shared-ux-prompt-not-found'; + +const defaultTitle = i18n.translate('share.urlService.redirect.components.Error.title', { + defaultMessage: 'Unable to open URL', + description: + 'Title displayed to user in redirect endpoint when redirection cannot be performed successfully.', +}); + +const defaultBody = i18n.translate('share.urlService.redirect.components.Error.body', { + defaultMessage: + `Sorry, the object you're looking for can't be found at this URL.` + + ` It might have been removed or maybe it never existed.`, +}); + +export interface ErrorProps { + title?: string; + body?: string; + homeHref: string; + docTitle: ChromeDocTitle; + error: Error; +} + +export const RedirectEmptyPrompt: React.FC = ({ + title = defaultTitle, + body = defaultBody, + homeHref, + docTitle, + error, +}) => { + // eslint-disable-next-line no-console + console.error('Short URL redirect error', error); + + docTitle.change( + i18n.translate('share.urlService.redirect.components.docTitle', { defaultMessage: 'Not Found' }) + ); + + return ( + {title}} + body={

{body}

} + actions={ + + {i18n.translate('share.urlService.redirect.components.Error.homeButton', { + defaultMessage: 'Back to home', + })} + + } + /> + ); +}; diff --git a/src/plugins/share/public/url_service/redirect/components/error.tsx b/src/plugins/share/public/url_service/redirect/components/error.tsx deleted file mode 100644 index c7e9e1a9f109..000000000000 --- a/src/plugins/share/public/url_service/redirect/components/error.tsx +++ /dev/null @@ -1,53 +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 * as React from 'react'; -import { - EuiEmptyPrompt, - EuiCallOut, - EuiCodeBlock, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiText, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -const defaultTitle = i18n.translate('share.urlService.redirect.components.Error.title', { - defaultMessage: 'Redirection error', - description: - 'Title displayed to user in redirect endpoint when redirection cannot be performed successfully.', -}); - -export interface ErrorProps { - title?: string; - error: Error; -} - -export const Error: React.FC = ({ title = defaultTitle, error }) => { - return ( - {title}} - body={ - - - - {error.message} - - - - - {error.stack ? error.stack : ''} - - - } - /> - ); -}; diff --git a/src/plugins/share/public/url_service/redirect/components/page.tsx b/src/plugins/share/public/url_service/redirect/components/page.tsx index b64c87cd374c..e06461a91b64 100644 --- a/src/plugins/share/public/url_service/redirect/components/page.tsx +++ b/src/plugins/share/public/url_service/redirect/components/page.tsx @@ -8,21 +8,31 @@ import * as React from 'react'; import useObservable from 'react-use/lib/useObservable'; + import { EuiPageTemplate } from '@elastic/eui'; -import { ThemeServiceSetup } from '@kbn/core/public'; +import type { CustomBrandingSetup } from '@kbn/core-custom-branding-browser'; +import type { ChromeDocTitle, ThemeServiceSetup } from '@kbn/core/public'; import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; -import { CustomBrandingStart } from '@kbn/core-custom-branding-browser'; -import { Error } from './error'; -import { RedirectManager } from '../redirect_manager'; + +import type { RedirectManager } from '../redirect_manager'; +import { RedirectEmptyPrompt } from './empty_prompt'; import { Spinner } from './spinner'; export interface PageProps { + homeHref: string; + docTitle: ChromeDocTitle; + customBranding: CustomBrandingSetup; manager: Pick; theme: ThemeServiceSetup; - customBranding: CustomBrandingStart; } -export const Page: React.FC = ({ manager, theme, customBranding }) => { +export const Page: React.FC = ({ + manager, + homeHref, + customBranding, + docTitle, + theme, +}) => { const error = useObservable(manager.error$); const hasCustomBranding = useObservable(customBranding.hasCustomBranding$); @@ -30,7 +40,7 @@ export const Page: React.FC = ({ manager, theme, customBranding }) => return ( - + ); diff --git a/src/plugins/share/public/url_service/redirect/redirect_manager.ts b/src/plugins/share/public/url_service/redirect/redirect_manager.ts index 330ded65a5b8..54c58ee3a626 100644 --- a/src/plugins/share/public/url_service/redirect/redirect_manager.ts +++ b/src/plugins/share/public/url_service/redirect/redirect_manager.ts @@ -8,15 +8,16 @@ import type { CoreSetup } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; -import { BehaviorSubject } from 'rxjs'; -import type { Location } from 'history'; import { migrateToLatest } from '@kbn/kibana-utils-plugin/common'; +import type { Location } from 'history'; +import { BehaviorSubject } from 'rxjs'; import type { UrlService } from '../../../common/url_service'; -import { parseSearchParams, RedirectOptions } from '../../../common/url_service/locators/redirect'; import { LEGACY_SHORT_URL_LOCATOR_ID, LegacyShortUrlLocatorParams, } from '../../../common/url_service/locators/legacy_short_url_locator'; +import { parseSearchParams, RedirectOptions } from '../../../common/url_service/locators/redirect'; +import { getHomeHref } from '../../lib/get_home_href'; export interface RedirectManagerDependencies { url: UrlService; @@ -28,18 +29,27 @@ export class RedirectManager { constructor(public readonly deps: RedirectManagerDependencies) {} public registerLocatorRedirectApp(core: CoreSetup) { - core.application.register({ + const { application, customBranding, http, theme } = core; + + application.register({ id: 'r', title: 'Redirect endpoint', chromeless: true, mount: async (params) => { const { render } = await import('./render'); + const [start] = await core.getStartServices(); + const { chrome, uiSettings } = start; + const unmount = render(params.element, { manager: this, - theme: core.theme, - customBranding: core.customBranding, + customBranding, + docTitle: chrome.docTitle, + theme, + homeHref: getHomeHref(http, uiSettings), }); + this.onMount(params.history.location); + return () => { unmount(); }; diff --git a/src/plugins/share/tsconfig.json b/src/plugins/share/tsconfig.json index e6100a773081..afbf9641579b 100644 --- a/src/plugins/share/tsconfig.json +++ b/src/plugins/share/tsconfig.json @@ -17,6 +17,8 @@ "@kbn/react-kibana-context-theme", "@kbn/core-analytics-browser", "@kbn/shared-ux-error-boundary", + "@kbn/core-chrome-browser", + "@kbn/shared-ux-prompt-not-found", ], "exclude": [ "target/**/*", diff --git a/test/functional/apps/sharing/_short_urls.ts b/test/functional/apps/sharing/_short_urls.ts new file mode 100644 index 000000000000..9d8994a773a3 --- /dev/null +++ b/test/functional/apps/sharing/_short_urls.ts @@ -0,0 +1,51 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + describe('Short URLs', () => { + const PageObjects = getPageObjects(['common']); + const browser = getService('browser'); + const log = getService('log'); + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + + it('shows Page for missing short URL', async () => { + await PageObjects.common.navigateToApp('home'); + + // go to 404 + const currentUrl = await browser.getCurrentUrl(); + log.info('Changing URL to missing short URL...'); + const newUrl = currentUrl.replace(/\/app\/home/, '/app/r/s/foofoo'); + await browser.get(newUrl); + + // check the page for 404 contents + log.info('Checking page title...'); + await retry.try(async () => { + const title = await browser.getTitle(); + expect(title).to.be('Not Found - Elastic'); + }); + + await retry.try(async () => { + await testSubjects.existOrFail('redirectErrorEmptyPromptBody'); + }); + + // click "back to home" button + log.info('Clicking the prompt button...'); + await testSubjects.click('redirectErrorEmptyPromptButton'); + + // check the page + await retry.try(async () => { + const title = await browser.getTitle(); + expect(title).to.be('Home - Elastic'); + }); + }); + }); +} diff --git a/test/functional/apps/sharing/config.ts b/test/functional/apps/sharing/config.ts new file mode 100644 index 000000000000..e487d31dcb65 --- /dev/null +++ b/test/functional/apps/sharing/config.ts @@ -0,0 +1,18 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/test/functional/apps/sharing/index.ts b/test/functional/apps/sharing/index.ts new file mode 100644 index 000000000000..9cbd715398e7 --- /dev/null +++ b/test/functional/apps/sharing/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Sharing features', () => { + loadTestFile(require.resolve('./_short_urls')); + }); +} diff --git a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx index 2da08344a70a..ebd09f7839fb 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx @@ -59,14 +59,14 @@ const StyledGraphControlsColumn = styled.div` } `; +const COLUMN_WIDTH = ['fit-content(10em)', 'auto']; + const StyledEuiDescriptionListTitle = styled(EuiDescriptionListTitle)` text-transform: uppercase; - max-width: 25%; `; const StyledEuiDescriptionListDescription = styled(EuiDescriptionListDescription)` - min-width: 75%; - width: 75%; + lineheight: '2.2em'; // lineHeight to align center vertically `; const StyledEuiButtonIcon = styled(EuiButtonIcon)` @@ -386,52 +386,35 @@ const SchemaInformation = ({ <> - + {i18n.translate('xpack.securitySolution.resolver.graphControls.schemaSource', { defaultMessage: 'source', })} - + {sourceAndSchema?.dataSource ?? unknownSchemaValue} - - + + {i18n.translate('xpack.securitySolution.resolver.graphControls.schemaID', { defaultMessage: 'id', })} - + {sourceAndSchema?.schema.id ?? unknownSchemaValue} - - + + {i18n.translate('xpack.securitySolution.resolver.graphControls.schemaEdge', { defaultMessage: 'edge', })} - + {sourceAndSchema?.schema.parent ?? unknownSchemaValue} - + @@ -493,14 +476,12 @@ const NodeLegend = ({ <> - + - + {i18n.translate( 'xpack.securitySolution.resolver.graphControls.runningProcessCube', @@ -521,10 +499,7 @@ const NodeLegend = ({ )} - + - + {i18n.translate( 'xpack.securitySolution.resolver.graphControls.terminatedProcessCube', @@ -545,10 +517,7 @@ const NodeLegend = ({ )} - + - + {i18n.translate( 'xpack.securitySolution.resolver.graphControls.currentlyLoadingCube', @@ -569,10 +535,7 @@ const NodeLegend = ({ )} - + - + {i18n.translate('xpack.securitySolution.resolver.graphControls.errorCube', { defaultMessage: 'Error Process', diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx index 64c94f76f86a..07ef565865d8 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx @@ -11,18 +11,11 @@ import React, { memo, useMemo, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import type { EuiBreadcrumb } from '@elastic/eui'; -import { - EuiSpacer, - EuiText, - EuiDescriptionList, - EuiHorizontalRule, - EuiTextColor, - EuiTitle, -} from '@elastic/eui'; +import { EuiSpacer, EuiText, EuiHorizontalRule, EuiTextColor, EuiTitle } from '@elastic/eui'; import styled from 'styled-components'; import { useSelector } from 'react-redux'; import { StyledPanel } from '../styles'; -import { BoldCode, StyledTime } from './styles'; +import { StyledDescriptionList, BoldCode, StyledTime } from './styles'; import { GeneratedText } from '../generated_text'; import { CopyablePanelField } from './copyable_panel_field'; import { Breadcrumbs } from './breadcrumbs'; @@ -329,16 +322,6 @@ function EventDetailBreadcrumbs({ return ; } -const StyledDescriptionList = memo(styled(EuiDescriptionList)` - .euiDescriptionList__title { - word-break: normal; - } - .euiDescriptionList__title, - .euiDescriptionList__description { - overflow-wrap: break-word; - } -`); - // Also prevents horizontal scrollbars on long descriptive names const StyledDescriptiveName = memo(styled(EuiText)` padding-right: 1em; diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx index ab762ec8a454..c258860e4f35 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx @@ -35,6 +35,8 @@ const StyledCubeForProcess = styled(CubeForProcess)` position: relative; `; +const COLUMN_WIDTH = ['fit-content(10em)', 'auto']; + const nodeDetailError = i18n.translate('xpack.securitySolution.resolver.panel.nodeDetail.Error', { defaultMessage: 'Node details were unable to be retrieved', }); @@ -249,6 +251,7 @@ const NodeDetailView = memo(function ({