[Shared UX] Add <NotFound /> prompt (#145598)

## Summary

Creates a shared `<NotFound />` prompt to be used when any given
consumer needs to show a 404 error.

<img width="1278" alt="Screenshot 2022-11-17 at 18 06 12"
src="https://user-images.githubusercontent.com/57448/202511151-a35f489b-d988-46cc-9810-5fc725e29b18.png">

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Alejandro Fernández Gómez 2022-12-01 15:44:56 +01:00 committed by GitHub
parent 137d178d71
commit df41bfa9fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 393 additions and 0 deletions

1
.github/CODEOWNERS vendored
View file

@ -1056,6 +1056,7 @@ packages/shared-ux/page/solution_nav @elastic/kibana-global-experience
packages/shared-ux/prompt/no_data_views/impl @elastic/kibana-global-experience
packages/shared-ux/prompt/no_data_views/mocks @elastic/kibana-global-experience
packages/shared-ux/prompt/no_data_views/types @elastic/kibana-global-experience
packages/shared-ux/prompt/not_found @elastic/kibana-global-experience
packages/shared-ux/router/impl @elastic/kibana-global-experience
packages/shared-ux/router/mocks @elastic/kibana-global-experience
packages/shared-ux/router/types @elastic/kibana-global-experience

View file

@ -418,6 +418,7 @@
"@kbn/shared-ux-prompt-no-data-views": "link:bazel-bin/packages/shared-ux/prompt/no_data_views/impl",
"@kbn/shared-ux-prompt-no-data-views-mocks": "link:bazel-bin/packages/shared-ux/prompt/no_data_views/mocks",
"@kbn/shared-ux-prompt-no-data-views-types": "link:bazel-bin/packages/shared-ux/prompt/no_data_views/types",
"@kbn/shared-ux-prompt-not-found": "link:bazel-bin/packages/shared-ux/prompt/not_found",
"@kbn/shared-ux-router-mocks": "link:bazel-bin/packages/shared-ux/router/mocks",
"@kbn/shared-ux-services": "link:bazel-bin/packages/kbn-shared-ux-services",
"@kbn/shared-ux-storybook": "link:bazel-bin/packages/kbn-shared-ux-storybook",

View file

@ -366,6 +366,7 @@ filegroup(
"//packages/shared-ux/prompt/no_data_views/impl:build",
"//packages/shared-ux/prompt/no_data_views/mocks:build",
"//packages/shared-ux/prompt/no_data_views/types:build",
"//packages/shared-ux/prompt/not_found:build",
"//packages/shared-ux/router/impl:build",
"//packages/shared-ux/router/mocks:build",
"//packages/shared-ux/router/types:build",
@ -714,6 +715,7 @@ filegroup(
"//packages/shared-ux/page/solution_nav:build_types",
"//packages/shared-ux/prompt/no_data_views/impl:build_types",
"//packages/shared-ux/prompt/no_data_views/mocks:build_types",
"//packages/shared-ux/prompt/not_found:build_types",
"//packages/shared-ux/router/impl:build_types",
"//packages/shared-ux/router/mocks:build_types",
"//packages/shared-ux/storybook/config:build_types",

View file

@ -0,0 +1,142 @@
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 = "errors"
PKG_REQUIRE_NAME = "@kbn/shared-ux-prompt-error"
SOURCE_FILES = glob(
[
"**/*.ts",
"**/*.tsx",
"**/*.png",
],
exclude = [
"**/*.config.js",
"**/*.mock.*",
"**/*.test.*",
"**/*.stories.*",
"**/__snapshots__/**",
"**/integration_tests/**",
"**/mocks/**",
"**/scripts/**",
"**/storybook/**",
"**/test_fixtures/**",
"**/test_helpers/**",
],
)
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//react",
"@npm//@elastic/eui",
"//packages/kbn-i18n"
]
# 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//@types/node",
"@npm//@types/jest",
"@npm//@types/react",
"@npm//@elastic/eui",
"//packages/kbn-ambient-ui-types",
"//packages/kbn-i18n: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",
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"],
)
js_library(
name = "npm_module_types",
srcs = NPM_MODULE_EXTRA_FILES,
deps = RUNTIME_DEPS + [":target_node", ":target_web", ":tsc_types"],
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"],
)
filegroup(
name = "build_types",
srcs = [":npm_module_types"],
visibility = ["//visibility:public"],
)

View file

@ -0,0 +1,12 @@
---
id: sharedUX/Prompt/NotFound
slug: /shared-ux/prompt/not-found
title: Not Found Prompt
description: A prompt to be displayed when a page or a resource does not exist.
tags: ['shared-ux', 'component', 'prompt', 'not-found']
date: 2022-02-09
---
Sometimes the user tries to go to a page that doesn't exist, because the URL is broken or because they try to load a resource that does not exist. For those cases we want to show a standard 404 error.
The default call to action is a "Go back" button that simulates the browser's "Back" behaviour. Consumers can specify their own CTA's with the `actions` prop.

View 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 { NotFoundPrompt } from './src/not_found_prompt';

View file

@ -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.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../../..',
roots: ['<rootDir>/packages/shared-ux/prompt/not_found'],
};

View file

@ -0,0 +1,7 @@
{
"type": "shared-common",
"id": "@kbn/shared-ux-prompt-not-found",
"owner": "@elastic/kibana-global-experience",
"runtimeDeps": [],
"typeDeps": []
}

View file

@ -0,0 +1,9 @@
{
"name": "@kbn/shared-ux-prompt-not-found",
"private": true,
"version": "1.0.0",
"main": "./target_node/index.js",
"browser": "./target_web/index.js",
"types": "./target_types/index.d.ts",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

View file

@ -0,0 +1,66 @@
/*
* 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, EuiButtonEmpty, EuiPageTemplate } from '@elastic/eui';
import React from 'react';
import { Meta, Story } from '@storybook/react';
import mdx from '../README.mdx';
import { NotFoundPrompt } from './not_found_prompt';
export default {
title: 'Not found/Prompt',
description:
'A component to display when the user reaches a page or tries to load a resource that does not exist',
parameters: {
docs: {
page: mdx,
},
},
argTypes: {
onClick: { action: 'clicked' },
},
} as Meta;
export const EmptyPage: Story = () => {
return (
<EuiPageTemplate>
<EuiPageTemplate.Section alignment="center">
<NotFoundPrompt />
</EuiPageTemplate.Section>
</EuiPageTemplate>
);
};
export const PageWithSidebar: Story = () => {
return (
<EuiPageTemplate panelled>
<EuiPageTemplate.Sidebar>sidebar</EuiPageTemplate.Sidebar>
<NotFoundPrompt />
</EuiPageTemplate>
);
};
export const CustomActions: Story = (args) => {
return (
<EuiPageTemplate>
<EuiPageTemplate.Section alignment="center">
<NotFoundPrompt
actions={[
<EuiButton fill color="primary" onClick={args.onClick}>
Go home
</EuiButton>,
<EuiButtonEmpty iconType="search" onClick={args.onClick}>
Go to discover
</EuiButtonEmpty>,
]}
/>
</EuiPageTemplate.Section>
</EuiPageTemplate>
);
};

View 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.
*/
import React from 'react';
import { render, mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { NotFoundPrompt } from './not_found_prompt';
describe('<NotFoundPrompt />', () => {
it('renders', () => {
const component = render(<NotFoundPrompt />);
expect(component.text()).toContain('Page not found');
});
it('has a default action with a "Go back" button', () => {
const component = mount(<NotFoundPrompt />);
const goBackButton = component.find('EuiButtonEmpty');
expect(goBackButton.text()).toBe('Go back');
const backSpy = jest.spyOn(history, 'back');
act(() => {
goBackButton.simulate('click');
});
expect(backSpy).toHaveBeenCalled();
backSpy.mockRestore();
});
it('Renders custom actions', () => {
const actions = [<button>I am a button</button>];
const component = render(<NotFoundPrompt actions={actions} />);
expect(component.text()).toContain('I am a button');
});
});

View file

@ -0,0 +1,76 @@
/*
* 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, { useEffect, useState, useCallback, useMemo } from 'react';
import {
EuiButtonEmpty,
EuiEmptyPrompt,
EuiEmptyPromptProps,
EuiImage,
useEuiTheme,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
const NOT_FOUND_TITLE = i18n.translate('sharedUXPackages.prompt.errors.notFound.title', {
defaultMessage: 'Page not found',
});
const NOT_FOUND_BODY = i18n.translate('sharedUXPackages.prompt.errors.notFound.body', {
defaultMessage:
"Sorry, the page you're looking for can't be found. It might have been removed or renamed, or maybe it never existed at all.",
});
const NOT_FOUND_GO_BACK = i18n.translate('sharedUXPackages.prompt.errors.notFound.goBacklabel', {
defaultMessage: 'Go back',
});
interface NotFoundProps {
/** Array of buttons, links and other actions to show at the bottom of the `EuiEmptyPrompt`. Defaults to a "Back" button. */
actions?: EuiEmptyPromptProps['actions'];
}
/**
* Predefined `EuiEmptyPrompt` for 404 pages.
*/
export const NotFoundPrompt = ({ actions }: NotFoundProps) => {
const { colorMode } = useEuiTheme();
const [imageSrc, setImageSrc] = useState<string>();
const goBack = useCallback(() => history.back(), []);
const DEFAULT_ACTIONS = useMemo(
() => [
<EuiButtonEmpty iconType="arrowLeft" flush="both" onClick={goBack}>
{NOT_FOUND_GO_BACK}
</EuiButtonEmpty>,
],
[goBack]
);
useEffect(() => {
const loadImage = async () => {
const { default: imgSrc } = await import(
`./assets/404_astronaut_${colorMode.toLowerCase()}.png`
);
setImageSrc(imgSrc);
};
loadImage();
}, [colorMode]);
const icon = imageSrc ? <EuiImage src={imageSrc} alt="" /> : null;
return (
<EuiEmptyPrompt
color="subdued"
titleSize="m"
icon={icon}
title={<h2>{NOT_FOUND_TITLE}</h2>}
body={NOT_FOUND_BODY}
actions={actions ?? DEFAULT_ACTIONS}
/>
);
};

View file

@ -0,0 +1,10 @@
{
"extends": "../../../../tsconfig.bazel.json",
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "target_types",
"types": ["jest", "node", "react", "@kbn/ambient-ui-types"]
},
"include": ["**/*.ts", "**/*.tsx"]
}

View file

@ -720,6 +720,8 @@
"@kbn/shared-ux-prompt-no-data-views-mocks/*": ["packages/shared-ux/prompt/no_data_views/mocks/*"],
"@kbn/shared-ux-prompt-no-data-views-types": ["packages/shared-ux/prompt/no_data_views/types"],
"@kbn/shared-ux-prompt-no-data-views-types/*": ["packages/shared-ux/prompt/no_data_views/types/*"],
"@kbn/shared-ux-prompt-not-found": ["packages/shared-ux/prompt/not_found"],
"@kbn/shared-ux-prompt-not-found/*": ["packages/shared-ux/prompt/not_found/*"],
"@kbn/shared-ux-router": ["packages/shared-ux/router/impl"],
"@kbn/shared-ux-router/*": ["packages/shared-ux/router/impl/*"],
"@kbn/shared-ux-router-mocks": ["packages/shared-ux/router/mocks"],

View file

@ -4021,6 +4021,10 @@
version "0.0.0"
uid ""
"@kbn/shared-ux-prompt-not-found@link:bazel-bin/packages/shared-ux/prompt/not_found":
version "0.0.0"
uid ""
"@kbn/shared-ux-prompt-no-data-views-mocks@link:bazel-bin/packages/shared-ux/prompt/no_data_views/mocks":
version "0.0.0"
uid ""