mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
137d178d71
commit
df41bfa9fe
17 changed files with 393 additions and 0 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
142
packages/shared-ux/prompt/not_found/BUILD.bazel
Normal file
142
packages/shared-ux/prompt/not_found/BUILD.bazel
Normal 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"],
|
||||
)
|
12
packages/shared-ux/prompt/not_found/README.mdx
Normal file
12
packages/shared-ux/prompt/not_found/README.mdx
Normal 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.
|
9
packages/shared-ux/prompt/not_found/index.ts
Normal file
9
packages/shared-ux/prompt/not_found/index.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 { NotFoundPrompt } from './src/not_found_prompt';
|
13
packages/shared-ux/prompt/not_found/jest.config.js
Normal file
13
packages/shared-ux/prompt/not_found/jest.config.js
Normal 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'],
|
||||
};
|
7
packages/shared-ux/prompt/not_found/kibana.jsonc
Normal file
7
packages/shared-ux/prompt/not_found/kibana.jsonc
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/shared-ux-prompt-not-found",
|
||||
"owner": "@elastic/kibana-global-experience",
|
||||
"runtimeDeps": [],
|
||||
"typeDeps": []
|
||||
}
|
9
packages/shared-ux/prompt/not_found/package.json
Normal file
9
packages/shared-ux/prompt/not_found/package.json
Normal 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 |
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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');
|
||||
});
|
||||
});
|
76
packages/shared-ux/prompt/not_found/src/not_found_prompt.tsx
Normal file
76
packages/shared-ux/prompt/not_found/src/not_found_prompt.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
10
packages/shared-ux/prompt/not_found/tsconfig.json
Normal file
10
packages/shared-ux/prompt/not_found/tsconfig.json
Normal 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"]
|
||||
}
|
|
@ -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"],
|
||||
|
|
|
@ -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 ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue