diff --git a/.buildkite/scripts/steps/security/third_party_packages.txt b/.buildkite/scripts/steps/security/third_party_packages.txt index 34d1235a7900f..68fe8f71bb5a4 100644 --- a/.buildkite/scripts/steps/security/third_party_packages.txt +++ b/.buildkite/scripts/steps/security/third_party_packages.txt @@ -7,3 +7,5 @@ tree-dump @opentelemetry/exporter-metrics-otlp-http inversify @types/d3-color +canvas-confetti +@types/canvas-confetti diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bd71c8db653c9..1dbfae3270f99 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -641,6 +641,7 @@ src/platform/packages/shared/shared-ux/chrome/navigation @elastic/appex-sharedux src/platform/packages/shared/shared-ux/code_editor/impl @elastic/appex-sharedux src/platform/packages/shared/shared-ux/code_editor/mocks @elastic/appex-sharedux src/platform/packages/shared/shared-ux/error_boundary @elastic/appex-sharedux +src/platform/packages/shared/shared-ux/feedback_snippet/impl @elastic/appex-sharedux src/platform/packages/shared/shared-ux/file/context @elastic/appex-sharedux src/platform/packages/shared/shared-ux/file/file_picker/impl @elastic/appex-sharedux src/platform/packages/shared/shared-ux/file/file_upload/impl @elastic/appex-sharedux diff --git a/package.json b/package.json index 6cab964c7759f..bad04df0eac76 100644 --- a/package.json +++ b/package.json @@ -972,6 +972,7 @@ "@kbn/shared-ux-card-no-data-types": "link:src/platform/packages/shared/shared-ux/card/no_data/types", "@kbn/shared-ux-chrome-navigation": "link:src/platform/packages/shared/shared-ux/chrome/navigation", "@kbn/shared-ux-error-boundary": "link:src/platform/packages/shared/shared-ux/error_boundary", + "@kbn/shared-ux-feedback-snippet": "link:src/platform/packages/shared/shared-ux/feedback_snippet/impl", "@kbn/shared-ux-file-context": "link:src/platform/packages/shared/shared-ux/file/context", "@kbn/shared-ux-file-image": "link:src/platform/packages/shared/shared-ux/file/image/impl", "@kbn/shared-ux-file-picker": "link:src/platform/packages/shared/shared-ux/file/file_picker/impl", @@ -1215,6 +1216,7 @@ "byte-size": "^9.0.1", "cacheable-lookup": "6", "camelcase-keys": "7.0.2", + "canvas-confetti": "^1.9.3", "canvg": "^3.0.9", "chalk": "^4.1.0", "cheerio": "^1.0.0-rc.12", @@ -1711,6 +1713,7 @@ "@types/base64-js": "^1.5.0", "@types/byte-size": "^8.1.2", "@types/cache-manager-fs-hash": "^0.0.5", + "@types/canvas-confetti": "^1.9.0", "@types/chance": "^1.0.0", "@types/chroma-js": "^2.1.0", "@types/chrome-remote-interface": "^0.31.14", diff --git a/renovate.json b/renovate.json index 3991d1ccf2c69..6303512909d8b 100644 --- a/renovate.json +++ b/renovate.json @@ -4678,6 +4678,25 @@ ], "minimumReleaseAge": "7 days", "enabled": true + }, + { + "groupName": "canvas-confetti", + "matchDepNames": [ + "canvas-confetti", + "@types/canvas-confetti" + ], + "reviewers": [ + "team:appex-sharedux" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:SharedUX", + "release_note:skip", + "backport:skip" + ], + "enabled": true } ], "customManagers": [ diff --git a/src/core/packages/chrome/browser-internal/src/ui/project/sidenav_v2/navigation/navigation.tsx b/src/core/packages/chrome/browser-internal/src/ui/project/sidenav_v2/navigation/navigation.tsx index 924981b2c11a2..558e638d08795 100644 --- a/src/core/packages/chrome/browser-internal/src/ui/project/sidenav_v2/navigation/navigation.tsx +++ b/src/core/packages/chrome/browser-internal/src/ui/project/sidenav_v2/navigation/navigation.tsx @@ -25,6 +25,7 @@ import { RedirectNavigationAppLinks } from './redirect_app_links'; import type { NavigationItems } from './to_navigation_items'; import { toNavigationItems } from './to_navigation_items'; import { PanelStateManager } from './panel_state_manager'; +import { NavigationFeedbackSnippet } from './navigation_feedback_snippet'; export interface ChromeNavigationProps { // sidenav state @@ -56,13 +57,14 @@ export const Navigation = (props: ChromeNavigationProps) => { return null; } - const { navItems, logoItem, activeItemId } = state; + const { navItems, logoItem, activeItemId, solutionId } = state; return ( } isCollapsed={props.isCollapsed} setWidth={props.setWidth} activeItemId={activeItemId} diff --git a/src/core/packages/chrome/browser-internal/src/ui/project/sidenav_v2/navigation/navigation_feedback_snippet.tsx b/src/core/packages/chrome/browser-internal/src/ui/project/sidenav_v2/navigation/navigation_feedback_snippet.tsx new file mode 100644 index 0000000000000..1666c52f286f9 --- /dev/null +++ b/src/core/packages/chrome/browser-internal/src/ui/project/sidenav_v2/navigation/navigation_feedback_snippet.tsx @@ -0,0 +1,53 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import { FeedbackSnippet } from '@kbn/shared-ux-feedback-snippet'; +import type { SolutionId } from '@kbn/core-chrome-browser'; +import { FormattedMessage } from '@kbn/i18n-react'; + +interface NavigationFeedbackSnippetProps { + solutionId: SolutionId; +} + +const feedbackSnippetId = 'sideNavigationFeedback'; + +const feedbackUrls: { [id in SolutionId]: string } = { + es: 'https://ela.st/search-nav-feedback', + chat: 'https://ela.st/search-nav-feedback', + oblt: 'https://ela.st/o11y-nav-feedback', + security: 'https://ela.st/security-nav-feedback', +}; + +const feedbackButtonMessage = ( + +); + +const promptViewMessage = ( + +); + +export const NavigationFeedbackSnippet = ({ solutionId }: NavigationFeedbackSnippetProps) => { + const feedbackSurveyUrl = feedbackUrls[solutionId]; + + return ( + + ); +}; diff --git a/src/core/packages/chrome/browser-internal/src/ui/project/sidenav_v2/navigation/to_navigation_items.tsx b/src/core/packages/chrome/browser-internal/src/ui/project/sidenav_v2/navigation/to_navigation_items.tsx index 8de93b25d9e8e..29b368b3def53 100644 --- a/src/core/packages/chrome/browser-internal/src/ui/project/sidenav_v2/navigation/to_navigation_items.tsx +++ b/src/core/packages/chrome/browser-internal/src/ui/project/sidenav_v2/navigation/to_navigation_items.tsx @@ -22,6 +22,7 @@ import type { SecondaryMenuSection, SideNavLogo, } from '@kbn/core-chrome-navigation/types'; +import type { SolutionId } from '@kbn/core-chrome-browser'; import { isActiveFromUrl } from '@kbn/shared-ux-chrome-navigation/src/utils'; import { AppDeepLinkIdToIcon } from './known_icons_mappings'; @@ -31,6 +32,7 @@ export interface NavigationItems { logoItem: SideNavLogo; navItems: NavigationStructure; activeItemId?: string; + solutionId: SolutionId; } /** @@ -313,7 +315,12 @@ export const toNavigationItems = ( // Check for duplicate icons warnAboutDuplicateIcons(logoItem, primaryItems); - return { logoItem, navItems: { primaryItems, footerItems }, activeItemId: deepestActiveItemId }; + return { + logoItem, + navItems: { primaryItems, footerItems }, + activeItemId: deepestActiveItemId, + solutionId: navigationTree.id, + }; }; // ===================== diff --git a/src/core/packages/chrome/browser-internal/tsconfig.json b/src/core/packages/chrome/browser-internal/tsconfig.json index 3a51f81bcbb53..c3d1b7f351dc4 100644 --- a/src/core/packages/chrome/browser-internal/tsconfig.json +++ b/src/core/packages/chrome/browser-internal/tsconfig.json @@ -69,7 +69,8 @@ "@kbn/core-chrome-layout-constants", "@kbn/core-feature-flags-browser", "@kbn/core-feature-flags-browser-mocks", - "@kbn/core-chrome-layout-feature-flags" + "@kbn/core-chrome-layout-feature-flags", + "@kbn/shared-ux-feedback-snippet" ], "exclude": [ "target/**/*", diff --git a/src/core/packages/chrome/navigation/src/components/navigation.tsx b/src/core/packages/chrome/navigation/src/components/navigation.tsx index d8a6f26b968d5..56aa293ae9875 100644 --- a/src/core/packages/chrome/navigation/src/components/navigation.tsx +++ b/src/core/packages/chrome/navigation/src/components/navigation.tsx @@ -30,6 +30,10 @@ export interface NavigationProps { * The active path for the navigation, used for highlighting the current item. */ activeItemId?: string; + /** + * Content to display inside the side panel footer. + */ + sidePanelFooter?: React.ReactNode; /** * Whether the navigation is collapsed. This can be controlled by the parent component. */ @@ -58,6 +62,7 @@ export const Navigation = ({ items, logo, setWidth, + sidePanelFooter, ...rest }: NavigationProps) => { const isMobile = useIsWithinBreakpoints(['xs', 's']); @@ -341,7 +346,7 @@ export const Navigation = ({ {isSidePanelOpen && sidePanelContent && ( - + { +export const SideNavPanel = ({ children, footer }: SideNavPanelProps): JSX.Element => { const ref = useRef(null); const { euiTheme } = useEuiTheme(); @@ -41,6 +42,8 @@ export const SideNavPanel = ({ children }: SideNavPanelProps): JSX.Element => { border-right: ${euiTheme.border.width.thin} ${euiTheme.colors.borderBaseSubdued} solid; height: 100%; scroll-padding-top: 44px; /* account for fixed header when scrolling to elements */ + display: flex; + flex-direction: column; `} color="subdued" // > For instance, only plain or transparent panels can have a border and/or shadow. @@ -50,7 +53,15 @@ export const SideNavPanel = ({ children }: SideNavPanelProps): JSX.Element => { borderRadius="none" grow={false} > - {children} +
+ {children} +
+ {footer} ); diff --git a/src/platform/packages/shared/shared-ux/feedback_snippet/impl/README.md b/src/platform/packages/shared/shared-ux/feedback_snippet/impl/README.md new file mode 100644 index 0000000000000..4a57f0b5ae4f0 --- /dev/null +++ b/src/platform/packages/shared/shared-ux/feedback_snippet/impl/README.md @@ -0,0 +1,23 @@ +# @kbn/shared-ux-feedback-snippet + +--- +id: sharedUX/Components/FeedbackSnippet +slug: /shared-ux/components/feedback_snippet +title: Feedback Snippet +summary: A component to gather user feedback that initially renders as a panel and becomes a button after interaction. +tags: ['shared-ux', 'component'] +date: 2025-09-11 +--- + +# Feedback Snippet +A snippet to gather user feedback. It initially renders as a panel, and once interacted with, it becomes a persistent button. It manages its own state (panel vs. button) based on user interaction tracked in `localStorage`. + +## Behavior +The component has two main states: +- **Panel:** On its first render for a user, the component displays as a full panel with the `promptViewMessage` and options to provide positive ("Yes") or negative ("No") feedback. A "Dismiss" (x) button is also available. +- **Button:** The component uses the provided `feedbackSnippetId` to track whether the user has interacted with it. If a value is present for that key, the component will render as a button instead. + +## Feedback Panel Views +- **Prompt:** The panel shows a custom `promptViewMessage` to gather feedback from the user. +- **Positive:** The panel shows a thank you message and then automatically dismisses itself. +- **Negative:** The panel updates to show a custom `surveyUrl` call-to-action button. The panel remains visible until the user explicitly dismisses it or navigates to the survey (which opens in a new tab). diff --git a/src/platform/packages/shared/shared-ux/feedback_snippet/impl/index.tsx b/src/platform/packages/shared/shared-ux/feedback_snippet/impl/index.tsx new file mode 100644 index 0000000000000..7d32c192359cf --- /dev/null +++ b/src/platform/packages/shared/shared-ux/feedback_snippet/impl/index.tsx @@ -0,0 +1,10 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { FeedbackSnippet } from './src/feedback_snippet'; diff --git a/src/platform/packages/shared/shared-ux/feedback_snippet/impl/jest.config.js b/src/platform/packages/shared/shared-ux/feedback_snippet/impl/jest.config.js new file mode 100644 index 0000000000000..abbcf564493c3 --- /dev/null +++ b/src/platform/packages/shared/shared-ux/feedback_snippet/impl/jest.config.js @@ -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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../../../../../../..', + roots: ['/src/platform/packages/shared/shared-ux/feedback_snippet'], +}; diff --git a/src/platform/packages/shared/shared-ux/feedback_snippet/impl/kibana.jsonc b/src/platform/packages/shared/shared-ux/feedback_snippet/impl/kibana.jsonc new file mode 100644 index 0000000000000..a7483e69279db --- /dev/null +++ b/src/platform/packages/shared/shared-ux/feedback_snippet/impl/kibana.jsonc @@ -0,0 +1,7 @@ +{ + "type": "shared-common", + "id": "@kbn/shared-ux-feedback-snippet", + "owner": "@elastic/appex-sharedux", + "group": "platform", + "visibility": "shared" +} diff --git a/src/platform/packages/shared/shared-ux/feedback_snippet/impl/package.json b/src/platform/packages/shared/shared-ux/feedback_snippet/impl/package.json new file mode 100644 index 0000000000000..3e830b3407921 --- /dev/null +++ b/src/platform/packages/shared/shared-ux/feedback_snippet/impl/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/shared-ux-feedback-snippet", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0" +} \ No newline at end of file diff --git a/src/platform/packages/shared/shared-ux/feedback_snippet/impl/src/confetti.tsx b/src/platform/packages/shared/shared-ux/feedback_snippet/impl/src/confetti.tsx new file mode 100644 index 0000000000000..a1f9678afb643 --- /dev/null +++ b/src/platform/packages/shared/shared-ux/feedback_snippet/impl/src/confetti.tsx @@ -0,0 +1,54 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { css } from '@emotion/react'; +import confetti from 'canvas-confetti'; +import React, { useEffect, useRef } from 'react'; + +const Confetti = () => { + const canvasRef = useRef(null); + + useEffect(() => { + let canvasConfetti: confetti.CreateTypes | null = null; + if (canvasRef.current) { + canvasConfetti = confetti.create(canvasRef.current, { + resize: true, + useWorker: true, + }); + canvasConfetti({ + origin: { y: 0 }, + startVelocity: 20, + spread: 90, + gravity: 1.3, + ticks: 250, + disableForReducedMotion: true, + }); + } + return () => { + canvasConfetti?.reset(); + }; + }, []); + + return ( + + ); +}; + +// We need to use the default export here because of the way React.lazy works +// eslint-disable-next-line import/no-default-export +export default Confetti; diff --git a/src/platform/packages/shared/shared-ux/feedback_snippet/impl/src/feedback_button.tsx b/src/platform/packages/shared/shared-ux/feedback_snippet/impl/src/feedback_button.tsx new file mode 100644 index 0000000000000..286a993fadc11 --- /dev/null +++ b/src/platform/packages/shared/shared-ux/feedback_snippet/impl/src/feedback_button.tsx @@ -0,0 +1,58 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import { css } from '@emotion/react'; + +import { EuiButtonEmpty, useEuiTheme } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +interface FeedbackButtonProps { + feedbackButtonMessage: React.ReactNode; + feedbackSnippetId: string; + handleOpenSurvey: () => void; +} + +const feedbackButtonAriaLabel = i18n.translate( + 'sharedUXPackages.feedbackSnippet.feedbackButtonLabel', + { + defaultMessage: 'Feedback button', + } +); + +/** + * A button to gather user feedback. + * It opens up a survey on a new tab. + * + */ +export const FeedbackButton = ({ + feedbackButtonMessage, + feedbackSnippetId, + handleOpenSurvey, +}: FeedbackButtonProps) => { + const { euiTheme } = useEuiTheme(); + + return ( + + {feedbackButtonMessage} + + ); +}; diff --git a/src/platform/packages/shared/shared-ux/feedback_snippet/impl/src/feedback_panel.tsx b/src/platform/packages/shared/shared-ux/feedback_snippet/impl/src/feedback_panel.tsx new file mode 100644 index 0000000000000..83f593d6a2946 --- /dev/null +++ b/src/platform/packages/shared/shared-ux/feedback_snippet/impl/src/feedback_panel.tsx @@ -0,0 +1,201 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import { css } from '@emotion/react'; + +import { i18n } from '@kbn/i18n'; +import { + EuiButton, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPanel, + EuiSpacer, + EuiText, + useEuiTheme, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { FeedbackView } from './feedback_snippet'; + +interface FeedbackPanelProps { + feedbackSnippetId: string; + feedbackView: FeedbackView; + promptViewMessage: React.ReactNode; + handleDismissPanel: () => void; + handleOpenSurveyAndDismissPanel: () => void; + handleNegativeFeedback: () => void; + handlePositiveFeedback: () => void; +} + +const ConfettiComponentLazy = React.lazy(() => import('./confetti')); + +const thumbUpIconLabel = i18n.translate( + 'sharedUXPackages.feedbackSnippet.feedbackPanel.thumbUpIconLabel', + { + defaultMessage: 'Thumb up', + } +); + +const thumbDownIconLabel = i18n.translate( + 'sharedUXPackages.feedbackSnippet.feedbackPanel.thumbDownIconLabel', + { + defaultMessage: 'Thumb down', + } +); + +const faceHappyIconLabel = i18n.translate( + 'sharedUXPackages.feedbackSnippet.feedbackPanel.faceHappyIconLabel', + { + defaultMessage: 'Happy face', + } +); + +/** + * A panel to gather user feedback. + * There are 3 available views: + * - Prompt: Ask the user for feedback + * - Positive: Thank the user for positive feedback + * - Negative: Ask the user for more information + */ +export const FeedbackPanel = ({ + feedbackSnippetId, + feedbackView, + promptViewMessage, + handleDismissPanel, + handleOpenSurveyAndDismissPanel, + handleNegativeFeedback, + handlePositiveFeedback, +}: FeedbackPanelProps) => { + const { euiTheme } = useEuiTheme(); + + const panelMessages = { + prompt: promptViewMessage, + positive: ( + }} + /> + ), + negative: ( + + ), + }; + + const closePanelIcon = ( + + + + ); + + const promptFooter = ( + <> + + + + + + + + + + + + ); + + const positiveFooter = ( + + + + ); + + const negativeFooter = ( + + + + ); + + const panelFooter = { + prompt: promptFooter, + positive: positiveFooter, + negative: negativeFooter, + }; + + return ( + + + + + {panelMessages[feedbackView]} + + + {/* Positive feedback view closes automatically */} + {feedbackView !== 'positive' && closePanelIcon} + + + + {panelFooter[feedbackView]} + + {feedbackView === 'positive' && } + + ); +}; diff --git a/src/platform/packages/shared/shared-ux/feedback_snippet/impl/src/feedback_snippet.tsx b/src/platform/packages/shared/shared-ux/feedback_snippet/impl/src/feedback_snippet.tsx new file mode 100644 index 0000000000000..6f7ff3cc5a02c --- /dev/null +++ b/src/platform/packages/shared/shared-ux/feedback_snippet/impl/src/feedback_snippet.tsx @@ -0,0 +1,111 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React, { useCallback, useEffect, useState } from 'react'; + +import { FeedbackButton } from './feedback_button'; +import { FeedbackPanel } from './feedback_panel'; + +const FEEDBACK_PANEL_POSITIVE_LIFETIME = 3000; + +interface FeedbackSnippetProps { + /** + * Message to display in the FeedbackButton. + */ + feedbackButtonMessage: React.ReactNode; + /** + * Id used for local storage and for HTML ids. + */ + feedbackSnippetId: string; + /** + * Message to display during the feedback prompt view. + */ + promptViewMessage: React.ReactNode; + /** + * Survey URL where the FeedbackButton will redirect to. + */ + surveyUrl: string; +} + +export type FeedbackView = 'prompt' | 'positive' | 'negative'; + +/** + * A snippet to gather user feedback. + * Initially a panel, once interacted with, it becomes a persistent button. + */ +export const FeedbackSnippet = ({ + feedbackButtonMessage, + feedbackSnippetId, + promptViewMessage, + surveyUrl, +}: FeedbackSnippetProps) => { + const [feedbackView, setFeedbackView] = useState('prompt'); + const [showPanel, setShowPanel] = useState(() => { + return localStorage.getItem(feedbackSnippetId) === null; + }); + + const handleOpenSurvey = () => { + window.open(surveyUrl, '_blank'); + }; + + const storeFeedbackInteraction = useCallback(() => { + localStorage.setItem(feedbackSnippetId, Date.now().toString()); + }, [feedbackSnippetId]); + + const handleDismissPanel = useCallback(() => { + setShowPanel(false); + storeFeedbackInteraction(); + }, [storeFeedbackInteraction]); + + const handleOpenSurveyAndDismissPanel = () => { + handleOpenSurvey(); + handleDismissPanel(); + }; + + const handlePositiveFeedback = () => { + setFeedbackView('positive'); + }; + + const handleNegativeFeedback = () => { + setFeedbackView('negative'); + // User might choose not to provide feedback and might not click on the x button + // In this case, we should not immediately hide the panel but only store the interaction + storeFeedbackInteraction(); + }; + + useEffect(() => { + let timer: number | undefined; + if (feedbackView === 'positive') { + timer = window.setTimeout(() => { + handleDismissPanel(); + }, FEEDBACK_PANEL_POSITIVE_LIFETIME); + } + return () => { + if (timer) window.clearTimeout(timer); + }; + }, [feedbackView, handleDismissPanel]); + + return showPanel ? ( + + ) : ( + + ); +}; diff --git a/src/platform/packages/shared/shared-ux/feedback_snippet/impl/src/index.ts b/src/platform/packages/shared/shared-ux/feedback_snippet/impl/src/index.ts new file mode 100644 index 0000000000000..0158fd5e6ba3a --- /dev/null +++ b/src/platform/packages/shared/shared-ux/feedback_snippet/impl/src/index.ts @@ -0,0 +1,10 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { FeedbackSnippet } from './feedback_snippet'; diff --git a/src/platform/packages/shared/shared-ux/feedback_snippet/impl/tsconfig.json b/src/platform/packages/shared/shared-ux/feedback_snippet/impl/tsconfig.json new file mode 100644 index 0000000000000..d4f0c2f4dbd8d --- /dev/null +++ b/src/platform/packages/shared/shared-ux/feedback_snippet/impl/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react", + "@emotion/react/types/css-prop", + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/i18n", + "@kbn/i18n-react", + ] +} diff --git a/src/platform/test/functional/page_objects/solution_navigation.ts b/src/platform/test/functional/page_objects/solution_navigation.ts index ac8e842a96a38..e30787bee795c 100644 --- a/src/platform/test/functional/page_objects/solution_navigation.ts +++ b/src/platform/test/functional/page_objects/solution_navigation.ts @@ -217,6 +217,7 @@ export function SolutionNavigationProvider(ctx: Pick feedbackSnippetPanelDismiss' + : 'sideNavfeedbackCallout > euiDismissCalloutButton'; + }, async expectExists() { - await testSubjects.existOrFail('sideNavfeedbackCallout', { timeout: TIMEOUT_CHECK }); + await testSubjects.existOrFail(await this.getFeedbackTestSubjectId(), { + timeout: TIMEOUT_CHECK, + }); }, async expectMissing() { - await testSubjects.missingOrFail('sideNavfeedbackCallout', { timeout: TIMEOUT_CHECK }); + return (await isV2()) + ? await testSubjects.existOrFail('feedbackSnippetButton', { + timeout: TIMEOUT_CHECK, + }) + : await testSubjects.missingOrFail(await this.getFeedbackTestSubjectId(), { + timeout: TIMEOUT_CHECK, + }); }, async dismiss() { - await testSubjects.click('sideNavfeedbackCallout > euiDismissCalloutButton'); + const feedbackTestSubjectId = await this.getFeedbackTestSubjectId(); + if (await testSubjects.exists(feedbackTestSubjectId, { timeout: TIMEOUT_CHECK })) { + await testSubjects.click(await this.getFeedbackDismissTestSubjectId()); + } }, }, }, diff --git a/tsconfig.base.json b/tsconfig.base.json index cceba60a3f230..4f5fd68730cc8 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1958,6 +1958,8 @@ "@kbn/shared-ux-chrome-navigation/*": ["src/platform/packages/shared/shared-ux/chrome/navigation/*"], "@kbn/shared-ux-error-boundary": ["src/platform/packages/shared/shared-ux/error_boundary"], "@kbn/shared-ux-error-boundary/*": ["src/platform/packages/shared/shared-ux/error_boundary/*"], + "@kbn/shared-ux-feedback-snippet": ["src/platform/packages/shared/shared-ux/feedback_snippet/impl"], + "@kbn/shared-ux-feedback-snippet/*": ["src/platform/packages/shared/shared-ux/feedback_snippet/impl/*"], "@kbn/shared-ux-file-context": ["src/platform/packages/shared/shared-ux/file/context"], "@kbn/shared-ux-file-context/*": ["src/platform/packages/shared/shared-ux/file/context/*"], "@kbn/shared-ux-file-image": ["src/platform/packages/shared/shared-ux/file/image/impl"], diff --git a/x-pack/solutions/search/test/functional_solution_sidenav/tests/search_sidenav.ts b/x-pack/solutions/search/test/functional_solution_sidenav/tests/search_sidenav.ts index 7a3102868842d..51a00848190c2 100644 --- a/x-pack/solutions/search/test/functional_solution_sidenav/tests/search_sidenav.ts +++ b/x-pack/solutions/search/test/functional_solution_sidenav/tests/search_sidenav.ts @@ -80,8 +80,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('renders a feedback callout', async function () { - await solutionNavigation.sidenav.skipIfV2(this); - + await solutionNavigation.sidenav.clickLink({ navId: 'stack_management' }); await solutionNavigation.sidenav.feedbackCallout.expectExists(); await solutionNavigation.sidenav.feedbackCallout.dismiss(); await solutionNavigation.sidenav.feedbackCallout.expectMissing(); diff --git a/yarn.lock b/yarn.lock index e88f0f3142a7c..78bd67dcce26d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7840,6 +7840,10 @@ version "0.0.0" uid "" +"@kbn/shared-ux-feedback-snippet@link:src/platform/packages/shared/shared-ux/feedback_snippet/impl": + version "0.0.0" + uid "" + "@kbn/shared-ux-file-context@link:src/platform/packages/shared/shared-ux/file/context": version "0.0.0" uid "" @@ -12394,6 +12398,11 @@ "@types/node" "*" "@types/responselike" "*" +"@types/canvas-confetti@^1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@types/canvas-confetti/-/canvas-confetti-1.9.0.tgz#d1077752e046413c9881fbb2ba34a70ebe3c1773" + integrity sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg== + "@types/chance@^1.0.0": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/chance/-/chance-1.0.1.tgz#c10703020369602c40dd9428cc6e1437027116df" @@ -16014,6 +16023,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001335, caniuse-lite@^1.0.30001718: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001720.tgz#c138cb6026d362be9d8d7b0e4bcd0183a850edfd" integrity sha512-Ec/2yV2nNPwb4DnTANEV99ZWwm3ZWfdlfkQbWSDDt+PsXEVYwlhPH8tdMaPunYTKKmz7AnHi2oNEi1GcmKCD8g== +canvas-confetti@^1.9.3: + version "1.9.3" + resolved "https://registry.yarnpkg.com/canvas-confetti/-/canvas-confetti-1.9.3.tgz#ef4c857420ad8045ab4abe8547261c8cdf229845" + integrity sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g== + canvg@^3.0.9: version "3.0.11" resolved "https://registry.yarnpkg.com/canvg/-/canvg-3.0.11.tgz#4b4290a6c7fa36871fac2b14e432eff33b33cf2b"