Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .buildkite/scripts/steps/security/third_party_packages.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ tree-dump
@opentelemetry/exporter-metrics-otlp-http
inversify
@types/d3-color
canvas-confetti
@types/canvas-confetti
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
19 changes: 19 additions & 0 deletions renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -56,13 +57,14 @@ export const Navigation = (props: ChromeNavigationProps) => {
return null;
}

const { navItems, logoItem, activeItemId } = state;
const { navItems, logoItem, activeItemId, solutionId } = state;

return (
<RedirectNavigationAppLinks application={props.application}>
<NavigationComponent
items={navItems}
logo={logoItem}
sidePanelFooter={<NavigationFeedbackSnippet solutionId={solutionId} />}
isCollapsed={props.isCollapsed}
setWidth={props.setWidth}
activeItemId={activeItemId}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = (
<FormattedMessage
id="core.ui.chrome.sideNavigation.sideNavigation.feedbackButtonText"
defaultMessage="Navigation feedback"
/>
);

const promptViewMessage = (
<FormattedMessage
id="core.ui.chrome.sideNavigation.feedbackPanel.promptTitle"
defaultMessage="How's the navigation working for you?"
/>
);

export const NavigationFeedbackSnippet = ({ solutionId }: NavigationFeedbackSnippetProps) => {
const feedbackSurveyUrl = feedbackUrls[solutionId];

return (
<FeedbackSnippet
feedbackButtonMessage={feedbackButtonMessage}
feedbackSnippetId={feedbackSnippetId}
promptViewMessage={promptViewMessage}
surveyUrl={feedbackSurveyUrl}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -31,6 +32,7 @@ export interface NavigationItems {
logoItem: SideNavLogo;
navItems: NavigationStructure;
activeItemId?: string;
solutionId: SolutionId;
}

/**
Expand Down Expand Up @@ -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,
};
};

// =====================
Expand Down
3 changes: 2 additions & 1 deletion src/core/packages/chrome/browser-internal/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/**/*",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -58,6 +62,7 @@ export const Navigation = ({
items,
logo,
setWidth,
sidePanelFooter,
...rest
}: NavigationProps) => {
const isMobile = useIsWithinBreakpoints(['xs', 's']);
Expand Down Expand Up @@ -341,7 +346,7 @@ export const Navigation = ({
</SideNav>

{isSidePanelOpen && sidePanelContent && (
<SideNav.Panel>
<SideNav.Panel footer={sidePanelFooter}>
<SecondaryMenu
badgeType={sidePanelContent.badgeType}
isPanel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { useRovingIndex } from '../../utils/use_roving_index';

export interface SideNavPanelProps {
children: ReactNode;
footer?: ReactNode;
}

/**
Expand All @@ -25,7 +26,7 @@ export interface SideNavPanelProps {
*
* TODO: pass ref to EuiPanel
*/
export const SideNavPanel = ({ children }: SideNavPanelProps): JSX.Element => {
export const SideNavPanel = ({ children, footer }: SideNavPanelProps): JSX.Element => {
const ref = useRef<HTMLDivElement | null>(null);

const { euiTheme } = useEuiTheme();
Expand All @@ -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.
Expand All @@ -50,7 +53,15 @@ export const SideNavPanel = ({ children }: SideNavPanelProps): JSX.Element => {
borderRadius="none"
grow={false}
>
{children}
<div
css={css`
flex-grow: 1;
overflow-y: auto;
`}
>
{children}
</div>
{footer}
</EuiPanel>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# @kbn/shared-ux-feedback-snippet
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't really need /impl/ filepath for the pacakge. the impl and types separation is unnecessary with the current setup and was added when we had a more constrained package system

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to know, I can remove it in an upcoming PR 👍


---
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).
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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: ['<rootDir>/src/platform/packages/shared/shared-ux/feedback_snippet'],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "shared-common",
"id": "@kbn/shared-ux-feedback-snippet",
"owner": "@elastic/appex-sharedux",
"group": "platform",
"visibility": "shared"
}
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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<HTMLCanvasElement>(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 (
<canvas
ref={canvasRef}
css={css`
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
`}
/>
);
};

// 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;
Loading