From dc945cd3027b6fba8c5594a9eaae3ab5782acfbc Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Thu, 1 Jan 2026 13:28:25 -0800 Subject: [PATCH 01/15] Add a story to show all the colors and their names --- src/lib/ColorPalette.stories.tsx | 269 +++++++++++++++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 src/lib/ColorPalette.stories.tsx diff --git a/src/lib/ColorPalette.stories.tsx b/src/lib/ColorPalette.stories.tsx new file mode 100644 index 0000000..e810da1 --- /dev/null +++ b/src/lib/ColorPalette.stories.tsx @@ -0,0 +1,269 @@ +import type {Meta, StoryObj} from '@storybook/react-vite'; +import {cx} from 'cva'; + +// Checkerboard pattern for showing colors against a neutral contrast background +const checkerboardStyle = { + backgroundImage: ` + linear-gradient(45deg, #808080 25%, transparent 25%), + linear-gradient(-45deg, #808080 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, #808080 75%), + linear-gradient(-45deg, transparent 75%, #808080 75%) + `, + backgroundSize: '8px 8px', + backgroundPosition: '0 0, 0 4px, 4px -4px, -4px 0px', + backgroundColor: '#c0c0c0', +}; + +interface ColorSwatchProps { + name: string; + cssVar: string; + tailwindClass: string; +} + +function ColorSwatch({name, cssVar, tailwindClass}: ColorSwatchProps) { + return ( +
+ {/* Outer container with checkerboard to show transparency/light colors */} +
+
+
+
+ {name} + {cssVar} +
+
+ ); +} + +interface ColorGroupProps { + title: string; + colors: ColorSwatchProps[]; +} + +function ColorGroup({title, colors}: ColorGroupProps) { + return ( +
+

{title}

+
+ {colors.map(color => ( + + ))} +
+
+ ); +} + +function ColorPalette() { + const colorGroups: ColorGroupProps[] = [ + { + title: 'Base', + colors: [ + {name: 'white', cssVar: 'var(--white)', tailwindClass: 'bg-white'}, + {name: 'black', cssVar: 'var(--black)', tailwindClass: 'bg-black'}, + {name: 'sentryPurple', cssVar: 'var(--sentry-purple)', tailwindClass: 'bg-sentryPurple'}, + ], + }, + { + title: 'Surface', + colors: [ + {name: 'surface-100', cssVar: 'var(--surface-100)', tailwindClass: 'bg-surface-100'}, + {name: 'surface-200', cssVar: 'var(--surface-200)', tailwindClass: 'bg-surface-200'}, + {name: 'surface-300', cssVar: 'var(--surface-300)', tailwindClass: 'bg-surface-300'}, + {name: 'surface-400', cssVar: 'var(--surface-400)', tailwindClass: 'bg-surface-400'}, + ], + }, + { + title: 'Translucent Gray', + colors: [ + {name: 'translucentGray-100', cssVar: 'var(--translucent-gray-100)', tailwindClass: 'bg-translucentGray-100'}, + {name: 'translucentGray-200', cssVar: 'var(--translucent-gray-200)', tailwindClass: 'bg-translucentGray-200'}, + ], + }, + { + title: 'Translucent Surface', + colors: [ + { + name: 'translucentSurface-100', + cssVar: 'var(--translucent-surface-100)', + tailwindClass: 'bg-translucentSurface-100', + }, + { + name: 'translucentSurface-200', + cssVar: 'var(--translucent-surface-200)', + tailwindClass: 'bg-translucentSurface-200', + }, + ], + }, + { + title: 'Gray', + colors: [ + {name: 'gray-100', cssVar: 'var(--gray-100)', tailwindClass: 'bg-gray-100'}, + {name: 'gray-200', cssVar: 'var(--gray-200)', tailwindClass: 'bg-gray-200'}, + {name: 'gray-300', cssVar: 'var(--gray-300)', tailwindClass: 'bg-gray-300'}, + {name: 'gray-400', cssVar: 'var(--gray-400)', tailwindClass: 'bg-gray-400'}, + {name: 'gray-500', cssVar: 'var(--gray-500)', tailwindClass: 'bg-gray-500'}, + ], + }, + { + title: 'Purple', + colors: [ + {name: 'purple-100', cssVar: 'var(--purple-100)', tailwindClass: 'bg-purple-100'}, + {name: 'purple-200', cssVar: 'var(--purple-200)', tailwindClass: 'bg-purple-200'}, + {name: 'purple-300', cssVar: 'var(--purple-300)', tailwindClass: 'bg-purple-300'}, + {name: 'purple-400', cssVar: 'var(--purple-400)', tailwindClass: 'bg-purple-400'}, + ], + }, + { + title: 'Blue', + colors: [ + {name: 'blue-100', cssVar: 'var(--blue-100)', tailwindClass: 'bg-blue-100'}, + {name: 'blue-200', cssVar: 'var(--blue-200)', tailwindClass: 'bg-blue-200'}, + {name: 'blue-300', cssVar: 'var(--blue-300)', tailwindClass: 'bg-blue-300'}, + {name: 'blue-400', cssVar: 'var(--blue-400)', tailwindClass: 'bg-blue-400'}, + ], + }, + { + title: 'Green', + colors: [ + {name: 'green-100', cssVar: 'var(--green-100)', tailwindClass: 'bg-green-100'}, + {name: 'green-200', cssVar: 'var(--green-200)', tailwindClass: 'bg-green-200'}, + {name: 'green-300', cssVar: 'var(--green-300)', tailwindClass: 'bg-green-300'}, + {name: 'green-400', cssVar: 'var(--green-400)', tailwindClass: 'bg-green-400'}, + ], + }, + { + title: 'Yellow', + colors: [ + {name: 'yellow-100', cssVar: 'var(--yellow-100)', tailwindClass: 'bg-yellow-100'}, + {name: 'yellow-200', cssVar: 'var(--yellow-200)', tailwindClass: 'bg-yellow-200'}, + {name: 'yellow-300', cssVar: 'var(--yellow-300)', tailwindClass: 'bg-yellow-300'}, + {name: 'yellow-400', cssVar: 'var(--yellow-400)', tailwindClass: 'bg-yellow-400'}, + ], + }, + { + title: 'Red', + colors: [ + {name: 'red-100', cssVar: 'var(--red-100)', tailwindClass: 'bg-red-100'}, + {name: 'red-200', cssVar: 'var(--red-200)', tailwindClass: 'bg-red-200'}, + {name: 'red-300', cssVar: 'var(--red-300)', tailwindClass: 'bg-red-300'}, + {name: 'red-400', cssVar: 'var(--red-400)', tailwindClass: 'bg-red-400'}, + ], + }, + { + title: 'Pink', + colors: [ + {name: 'pink-100', cssVar: 'var(--pink-100)', tailwindClass: 'bg-pink-100'}, + {name: 'pink-200', cssVar: 'var(--pink-200)', tailwindClass: 'bg-pink-200'}, + {name: 'pink-300', cssVar: 'var(--pink-300)', tailwindClass: 'bg-pink-300'}, + {name: 'pink-400', cssVar: 'var(--pink-400)', tailwindClass: 'bg-pink-400'}, + ], + }, + { + title: 'Shadow', + colors: [ + {name: 'shadow-light', cssVar: 'var(--shadow-light)', tailwindClass: 'bg-shadow-light'}, + {name: 'shadow-medium', cssVar: 'var(--shadow-medium)', tailwindClass: 'bg-shadow-medium'}, + {name: 'shadow-heavy', cssVar: 'var(--shadow-heavy)', tailwindClass: 'bg-shadow-heavy'}, + ], + }, + ]; + + return ( +
+

Color Palette

+

+ Colors adapt based on the current theme (light/dark). Toggle the theme to see how colors change. +

+
+ {colorGroups.map(group => ( + + ))} +
+
+ ); +} + +function RawColors() { + return ( +
+

Raw Colors (Theme-Independent)

+

These colors don't change with the theme.

+
+ + +
+
+ ); +} + +function UtilityColors() { + return ( +
+

Utility Colors

+
+
+
+
+
+ transparent +
+
+
+
+
+ current +
+
+
+
+
+ inherit +
+
+
+ ); +} + +interface StoryArgs { + theme: 'light' | 'dark'; +} + +const meta: Meta = { + title: 'Design System/Color Palette', + component: ColorPalette, + parameters: { + layout: 'fullscreen', + }, + argTypes: { + theme: { + control: 'radio', + options: ['light', 'dark'], + description: 'Toggle between light and dark theme', + }, + }, + args: { + theme: 'light', + }, + decorators: [ + (Story, context) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const AllColors: Story = { + render: () => ( +
+ +
+ +
+ +
+ ), +}; From f763244462b850457888c91252fe55f8e3b7faad Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Thu, 1 Jan 2026 22:07:54 -0800 Subject: [PATCH 02/15] Rewrite the unauth flow so its inside the normal EdgeLayout system Putting the unauth state inside the EdgeLayout system means that it can be drag+dropped, we can use bigger panels to show more infomation about whats configured and and the current state, it can be collapsed in the same way as a logged-in session, and finally we can include the Feature Flags panel even when the user is not logged-in because it uses local state only. --- src/lib/components/AppRouter.tsx | 73 ++--- src/lib/components/AssignedTo.tsx | 97 +------ src/lib/components/InfiniteListItems.tsx | 42 +-- src/lib/components/Media.stories.tsx | 266 ++++++++++++++++++ src/lib/components/Media.tsx | 89 ++++++ src/lib/components/Navigation.tsx | 231 --------------- src/lib/components/avatar/AvatarIcon.tsx | 89 ++++++ src/lib/components/base/Breadcrumbs.tsx | 33 +++ src/lib/components/base/Button.stories.tsx | 52 ++++ src/lib/components/base/Button.tsx | 30 ++ src/lib/components/base/InternalLink.tsx | 20 ++ .../{ => base}/LoadingSpinner.stories.tsx | 2 +- .../components/{ => base}/LoadingSpinner.tsx | 0 .../components/base/Placeholder.stories.tsx | 227 +++++++++++++++ src/lib/components/{ => base}/Placeholder.tsx | 21 +- src/lib/components/base/ScrollableList.tsx | 21 ++ .../components/{ => base}/SentryAppLink.tsx | 0 src/lib/components/icon/IconSlashForward.tsx | 10 + src/lib/components/layouts/CenterLayout.tsx | 19 -- .../components/layouts/EdgeLayout.stories.tsx | 8 +- src/lib/components/layouts/EdgeLayout.tsx | 8 +- src/lib/components/layouts/UnauthLayout.tsx | 68 ----- .../panels/featureFlags/FeatureFlagsPanel.tsx | 7 +- .../panels/feedback/FeedbackListItem.tsx | 2 +- .../panels/feedback/FeedbackPanel.tsx | 13 +- .../panels/issues/IssueListItem.tsx | 2 +- .../components/panels/issues/IssuesPanel.tsx | 13 +- .../nav/AuthNavigation.stories.tsx} | 10 +- src/lib/components/panels/nav/NavButton.tsx | 57 ++++ src/lib/components/panels/nav/NavGrabber.tsx | 21 ++ .../components/panels/nav/NavigationPanel.tsx | 87 ++++++ .../panels/nav}/useNavigationExpansion.ts | 0 .../panels/settings/ConfigPanel.tsx | 36 +++ .../panels/settings/CurrentUser.tsx | 49 ++++ .../components/panels/settings/ProxyState.tsx | 28 ++ .../panels/settings/SettingsPanel.tsx | 80 +++++- .../proxyState}/Connecting.stories.tsx | 2 +- .../settings/proxyState}/Connecting.tsx | 0 .../proxyState}/Disconnected.stories.tsx | 2 +- .../settings/proxyState}/Disconnected.tsx | 0 .../proxyState}/InvalidDomain.stories.tsx | 2 +- .../settings/proxyState}/InvalidDomain.tsx | 6 +- .../settings/proxyState}/Login.stories.tsx | 2 +- .../settings/proxyState}/Login.tsx | 18 +- .../panels/settings/proxyState/Logout.tsx | 13 + .../proxyState}/MissingProject.stories.tsx | 2 +- .../settings/proxyState}/MissingProject.tsx | 6 +- src/lib/components/unauth/UnauthPill.tsx | 116 -------- src/lib/context/ApiProxyContext.tsx | 21 +- src/lib/hooks/fetch/useSentryApi.ts | 2 +- .../hooks/useNavigateOnProxyStateChange.ts | 36 --- src/lib/utils/ApiProxy.ts | 3 +- 52 files changed, 1356 insertions(+), 686 deletions(-) create mode 100644 src/lib/components/Media.stories.tsx create mode 100644 src/lib/components/Media.tsx delete mode 100644 src/lib/components/Navigation.tsx create mode 100644 src/lib/components/avatar/AvatarIcon.tsx create mode 100644 src/lib/components/base/Breadcrumbs.tsx create mode 100644 src/lib/components/base/Button.stories.tsx create mode 100644 src/lib/components/base/Button.tsx create mode 100644 src/lib/components/base/InternalLink.tsx rename src/lib/components/{ => base}/LoadingSpinner.stories.tsx (90%) rename src/lib/components/{ => base}/LoadingSpinner.tsx (100%) create mode 100644 src/lib/components/base/Placeholder.stories.tsx rename src/lib/components/{ => base}/Placeholder.tsx (53%) create mode 100644 src/lib/components/base/ScrollableList.tsx rename src/lib/components/{ => base}/SentryAppLink.tsx (100%) create mode 100644 src/lib/components/icon/IconSlashForward.tsx delete mode 100644 src/lib/components/layouts/CenterLayout.tsx delete mode 100644 src/lib/components/layouts/UnauthLayout.tsx rename src/lib/components/{Navigation.stories.tsx => panels/nav/AuthNavigation.stories.tsx} (90%) create mode 100644 src/lib/components/panels/nav/NavButton.tsx create mode 100644 src/lib/components/panels/nav/NavGrabber.tsx create mode 100644 src/lib/components/panels/nav/NavigationPanel.tsx rename src/lib/{hooks => components/panels/nav}/useNavigationExpansion.ts (100%) create mode 100644 src/lib/components/panels/settings/ConfigPanel.tsx create mode 100644 src/lib/components/panels/settings/CurrentUser.tsx create mode 100644 src/lib/components/panels/settings/ProxyState.tsx rename src/lib/components/{unauth => panels/settings/proxyState}/Connecting.stories.tsx (87%) rename src/lib/components/{unauth => panels/settings/proxyState}/Connecting.tsx (100%) rename src/lib/components/{unauth => panels/settings/proxyState}/Disconnected.stories.tsx (86%) rename src/lib/components/{unauth => panels/settings/proxyState}/Disconnected.tsx (100%) rename src/lib/components/{unauth => panels/settings/proxyState}/InvalidDomain.stories.tsx (86%) rename src/lib/components/{unauth => panels/settings/proxyState}/InvalidDomain.tsx (79%) rename src/lib/components/{unauth => panels/settings/proxyState}/Login.stories.tsx (88%) rename src/lib/components/{unauth => panels/settings/proxyState}/Login.tsx (72%) create mode 100644 src/lib/components/panels/settings/proxyState/Logout.tsx rename src/lib/components/{unauth => panels/settings/proxyState}/MissingProject.stories.tsx (86%) rename src/lib/components/{unauth => panels/settings/proxyState}/MissingProject.tsx (76%) delete mode 100644 src/lib/components/unauth/UnauthPill.tsx delete mode 100644 src/lib/hooks/useNavigateOnProxyStateChange.ts diff --git a/src/lib/components/AppRouter.tsx b/src/lib/components/AppRouter.tsx index d6005ce..a98db7c 100644 --- a/src/lib/components/AppRouter.tsx +++ b/src/lib/components/AppRouter.tsx @@ -1,69 +1,72 @@ -import {Fragment} from 'react/jsx-runtime'; -import {Routes, Route, Outlet} from 'react-router-dom'; +import {Fragment, useEffect, type ReactNode} from 'react'; +import {Routes, Route, Outlet, useNavigate} from 'react-router-dom'; import DebugState from 'toolbar/components/DebugState'; -import EdgeLayout, {MainArea, NavArea} from 'toolbar/components/layouts/EdgeLayout'; -import UnauthLayout from 'toolbar/components/layouts/UnauthLayout'; -import Navigation from 'toolbar/components/Navigation'; +import DragDropPositionSurface from 'toolbar/components/DragDropPositionSurface'; +import EdgeLayout, {PanelArea, NavArea} from 'toolbar/components/layouts/EdgeLayout'; import FeatureFlagsPanel from 'toolbar/components/panels/featureFlags/FeatureFlagsPanel'; import FeedbackPanel from 'toolbar/components/panels/feedback/FeedbackPanel'; import IssuesPanel from 'toolbar/components/panels/issues/IssuesPanel'; +import NavigationPanel from 'toolbar/components/panels/nav/NavigationPanel'; +import ConfigPanel from 'toolbar/components/panels/settings/ConfigPanel'; import SettingsPanel from 'toolbar/components/panels/settings/SettingsPanel'; -import Connecting from 'toolbar/components/unauth/Connecting'; -import Disconnected from 'toolbar/components/unauth/Disconnected'; -import InvalidDomain from 'toolbar/components/unauth/InvalidDomain'; -import Login from 'toolbar/components/unauth/Login'; -import MissingProject from 'toolbar/components/unauth/MissingProject'; +import {useApiProxyState} from 'toolbar/context/ApiProxyContext'; import useClearQueryCacheOnProxyStateChange from 'toolbar/hooks/useClearQueryCacheOnProxyStateChange'; -import useNavigateOnProxyStateChange from 'toolbar/hooks/useNavigateOnProxyStateChange'; export default function AppRouter() { - useNavigateOnProxyStateChange(); useClearQueryCacheOnProxyStateChange(); return ( - - - }> - - - - }> - } /> - } /> - } /> - } /> - } /> - - - + + + + }> + + + }> + + } /> + } /> + + } /> + - + }> - } /> } /> } /> - } /> ); } + +function RequireAuth({children}: {children: ReactNode}) { + const navigate = useNavigate(); + const proxyState = useApiProxyState(); + + useEffect(() => { + if (proxyState !== 'logged-in') { + navigate('/'); + } + }, [proxyState, navigate]); + + return children; +} diff --git a/src/lib/components/AssignedTo.tsx b/src/lib/components/AssignedTo.tsx index 7ac3d59..51e91dc 100644 --- a/src/lib/components/AssignedTo.tsx +++ b/src/lib/components/AssignedTo.tsx @@ -1,20 +1,16 @@ -import {cx} from 'cva'; +import AvatarIcon from 'toolbar/components/avatar/AvatarIcon'; import {Tooltip, TooltipTrigger, TooltipContent} from 'toolbar/components/base/tooltip/Tooltip'; import type {GroupAssignedTo} from 'toolbar/sentryApi/types/group'; import type Member from 'toolbar/sentryApi/types/Member'; import type {OrganizationTeam} from 'toolbar/sentryApi/types/Organization'; -const initialsClassName = cx('flex size-2 items-center justify-center truncate text-[8px] text-white'); - -export default function AssignedTo({ - assignedTo, - teams: _teams, - members, -}: { +interface Props { assignedTo: GroupAssignedTo | null | undefined; teams: OrganizationTeam[] | undefined; members: Member[] | undefined; -}) { +} + +export default function AssignedTo({assignedTo, teams: _teams, members}: Props) { if (!assignedTo) { return ( @@ -28,87 +24,18 @@ export default function AssignedTo({ if (assignedTo.type === 'user') { const userAvatar = members?.filter(m => m.user?.id === assignedTo.id)[0]?.user?.avatar; - const displayName = assignedTo.name; const userAvatarUrl = userAvatar?.avatarType === 'gravatar' || userAvatar?.avatarType === 'upload' ? userAvatar?.avatarUrl : undefined; return ( - - - {userAvatarUrl ? ( - - ) : ( - - {getAvatarInitials(assignedTo.name)} - - )} - - Assigned to {displayName} - + ); } else if (assignedTo.type === 'team') { - return ( - - - - {getAvatarInitials(assignedTo.name)} - - - Assigned to {`#${assignedTo.name}`} - - ); - } -} - -const COLORS = [ - '#4674ca', // blue - '#315cac', // blue_dark - '#57be8c', // green - '#3fa372', // green_dark - '#f9a66d', // yellow_orange - '#ec5e44', // red - '#e63717', // red_dark - '#f868bc', // pink - '#6c5fc7', // purple - '#4e3fb4', // purple_dark - '#57b1be', // teal - '#847a8c', // gray -] as const; - -type Color = (typeof COLORS)[number]; - -function hashIdentifier(identifier: string) { - identifier += ''; - let hash = 0; - for (let i = 0; i < identifier.length; i++) { - hash += identifier.charCodeAt(i); - } - return hash; -} - -function getAvatarColor(identifier: string | undefined): Color { - // Gray if the identifier is not set - if (identifier === undefined) { - return '#847a8c' as Color; - } - - const id = hashIdentifier(identifier); - return COLORS[id % COLORS.length] || '#847a8c'; -} - -function getAvatarInitials(displayName: string | undefined) { - // split on spaces for names and '-' for teams - const names = (typeof displayName === 'string' && displayName.trim() ? displayName : '?').split(/[-\s]+/); - - // Use Array.from as slicing and substring() work on ucs2 segments which - // results in only getting half of any 4+ byte character. - let initials = names.at(0)?.charAt(0); - if (names.length > 1 && initials) { - initials += names[names.length - 1]?.charAt(0); + return ; } - return initials?.toUpperCase() || '?'; } diff --git a/src/lib/components/InfiniteListItems.tsx b/src/lib/components/InfiniteListItems.tsx index 419112e..332bf97 100644 --- a/src/lib/components/InfiniteListItems.tsx +++ b/src/lib/components/InfiniteListItems.tsx @@ -1,7 +1,8 @@ import type {InfiniteData, UseInfiniteQueryResult} from '@tanstack/react-query'; import {useVirtualizer, type VirtualItem} from '@tanstack/react-virtual'; import {useEffect, useRef} from 'react'; -import LoadingSpinner from 'toolbar/components/LoadingSpinner'; +import LoadingSpinner from 'toolbar/components/base/LoadingSpinner'; +import ScrollableList from 'toolbar/components/base/ScrollableList'; import type {ApiResult} from 'toolbar/types/api'; interface Props> { @@ -52,27 +53,26 @@ export default function InfiniteListItems -
-
    - {items.length ? null : emptyMessage()} - {items.map(virtualItem => { - const isLoaderRow = virtualItem.index > loadedRows.length - 1; - const item = loadedRows.at(virtualItem.index); + + {items.length ? null : emptyMessage()} + {items.map(virtualItem => { + const isLoaderRow = virtualItem.index > loadedRows.length - 1; + const item = loadedRows.at(virtualItem.index); - return ( -
  • - {isLoaderRow - ? hasNextPage - ? loadingMoreMessage() - : loadingCompleteMessage() - : item && itemRenderer({item, virtualItem})} -
  • - ); - })} -
-
-
+ return ( +
  • + {isLoaderRow + ? hasNextPage + ? loadingMoreMessage() + : loadingCompleteMessage() + : item && itemRenderer({item, virtualItem})} +
  • + ); + })} + ); } diff --git a/src/lib/components/Media.stories.tsx b/src/lib/components/Media.stories.tsx new file mode 100644 index 0000000..fe83e4b --- /dev/null +++ b/src/lib/components/Media.stories.tsx @@ -0,0 +1,266 @@ +import type {Meta, StoryObj} from '@storybook/react-vite'; +import AvatarIcon from 'toolbar/components/avatar/AvatarIcon'; +import Placeholder from 'toolbar/components/base/Placeholder'; +import IconIssues from 'toolbar/components/icon/IconIssues'; +import IconMegaphone from 'toolbar/components/icon/IconMegaphone'; +import IconSettings from 'toolbar/components/icon/IconSettings'; +import PlatformIcon from 'toolbar/components/icon/PlatformIcon'; +import Media from 'toolbar/components/Media'; + +const meta: Meta = { + title: 'Components/Media', + component: Media, + argTypes: { + size: { + control: 'select', + options: ['sm', 'md', 'lg'], + }, + }, + decorators: [ + Story => ( +
    + +
    + ), + ], +}; + +export default meta; +type Story = StoryObj; + +// Basic examples with different media types + +export const WithAvatar: Story = { + args: { + media: , + title: 'Jane Doe', + description: 'jane.doe@example.com', + size: 'md', + }, +}; + +export const WithTeamAvatar: Story = { + args: { + media: , + title: 'Platform Team', + description: '12 members', + size: 'md', + }, +}; + +export const WithAvatarUrl: Story = { + args: { + media: ( + + ), + title: 'Sentry User', + description: 'user@sentry.io', + size: 'md', + }, +}; + +export const WithPlatformIcon: Story = { + args: { + media: , + title: 'frontend-app', + description: 'javascript-react', + size: 'md', + }, +}; + +export const WithPlatformIconPython: Story = { + args: { + media: , + title: 'backend-api', + description: 'python-django', + size: 'md', + }, +}; + +export const WithIcon: Story = { + args: { + media: , + title: 'Settings', + description: 'Manage your preferences', + size: 'md', + }, +}; + +export const WithIssuesIcon: Story = { + args: { + media: , + title: 'TypeError: Cannot read property', + description: 'users/auth.js in handleLogin', + size: 'md', + }, +}; + +export const WithFeedbackIcon: Story = { + args: { + media: , + title: 'User Feedback', + description: 'The page is loading slowly', + size: 'md', + }, +}; + +// Size variants + +export const SizeSmall: Story = { + args: { + media: , + title: 'Small Size', + description: 'Compact layout', + size: 'sm', + }, +}; + +export const SizeMedium: Story = { + args: { + media: , + title: 'Medium Size', + description: 'Default layout', + size: 'md', + }, +}; + +export const SizeLarge: Story = { + args: { + media: , + title: 'Large Size', + description: 'Spacious layout', + size: 'lg', + }, +}; + +// Without description + +export const TitleOnly: Story = { + args: { + media: , + title: 'Node.js Application', + size: 'md', + }, +}; + +// All sizes comparison + +export const AllSizes: Story = { + render: () => ( +
    + } + title="Small (sm)" + description="Compact variant" + size="sm" + /> + } + title="Medium (md)" + description="Default variant" + size="md" + /> + } + title="Large (lg)" + description="Spacious variant" + size="lg" + /> +
    + ), +}; + +// Placeholder examples + +export const PlaceholderMedia: Story = { + args: { + media:
    , + title: 'Loading...', + description: 'Please wait', + size: 'md', + }, +}; + +export const AllPlaceholders: Story = { + render: () => ( +
    + } + title={
    } + description={
    } + size="sm" + /> + } + title={
    } + description={
    } + size="md" + /> + } + title={
    } + description={
    } + size="lg" + /> +
    + ), +}; + +export const PlaceholderSquareMedia: Story = { + render: () => ( +
    + } + title={
    } + description={
    } + size="sm" + /> + } + title={
    } + description={
    } + size="md" + /> + } + title={
    } + description={
    } + size="lg" + /> +
    + ), +}; + +// Mixed content showcase + +export const Showcase: Story = { + render: () => ( +
    + } + title="John Smith" + description="john@example.com" + size="md" + /> + } + title="my-nextjs-app" + description="javascript-nextjs" + size="md" + /> + } title="ReferenceError" description="index.js:42" size="md" /> + } + title="Backend Team" + description="8 members" + size="md" + /> +
    + ), +}; diff --git a/src/lib/components/Media.tsx b/src/lib/components/Media.tsx new file mode 100644 index 0000000..0c59281 --- /dev/null +++ b/src/lib/components/Media.tsx @@ -0,0 +1,89 @@ +import {cva, cx} from 'cva'; +import type {ReactNode} from 'react'; + +const containerClassName = cva('flex items-center', { + variants: { + size: { + sm: 'gap-0.75', + md: 'gap-1', + lg: 'gap-1.5', + }, + }, + defaultVariants: { + size: 'md', + }, +}); + +const mediaClassName = cva('flex shrink-0 items-center justify-center', { + variants: { + size: { + sm: 'size-2', + md: 'size-3', + lg: 'size-4', + }, + }, + defaultVariants: { + size: 'md', + }, +}); + +const titleClassName = cva('truncate font-medium', { + variants: { + size: { + sm: 'text-xs', + md: 'text-sm', + lg: 'text-base', + }, + }, + defaultVariants: { + size: 'md', + }, +}); + +const descriptionClassName = cva('truncate text-gray-300', { + variants: { + size: { + sm: 'text-[10px]', + md: 'text-xs', + lg: 'text-sm', + }, + }, + defaultVariants: { + size: 'md', + }, +}); + +interface Props { + /** + * Content rendered in the square left section (icon, avatar, image, etc.) + */ + media: ReactNode; + /** + * Primary text shown in the first row + */ + title: ReactNode; + /** + * Secondary text shown in the second row + */ + description?: ReactNode; + /** + * Size variant affecting spacing and typography + */ + size?: 'sm' | 'md' | 'lg'; + /** + * Additional className applied to the container + */ + className?: string; +} + +export default function Media({media, title, description, size = 'md', className}: Props) { + return ( +
    +
    {media}
    +
    +
    {title}
    + {description !== undefined &&
    {description}
    } +
    +
    + ); +} diff --git a/src/lib/components/Navigation.tsx b/src/lib/components/Navigation.tsx deleted file mode 100644 index ff29517..0000000 --- a/src/lib/components/Navigation.tsx +++ /dev/null @@ -1,231 +0,0 @@ -import type {Placement} from '@floating-ui/react'; -import {Transition} from '@headlessui/react'; -import {cva, cx} from 'cva'; -import {Fragment} from 'react'; -import type {MouseEvent} from 'react'; -import {NavLink, useLocation, useNavigate} from 'react-router-dom'; -import type {To} from 'react-router-dom'; -import ExternalLink from 'toolbar/components/base/ExternalLink'; -import Indicator from 'toolbar/components/base/Indicator'; -import {Menu, MenuItem} from 'toolbar/components/base/menu/Menu'; -import {Tooltip, TooltipTrigger, TooltipContent} from 'toolbar/components/base/tooltip/Tooltip'; -import IconClose from 'toolbar/components/icon/IconClose'; -import IconFlag from 'toolbar/components/icon/IconFlag'; -import IconIssues from 'toolbar/components/icon/IconIssues'; -import IconLock from 'toolbar/components/icon/IconLock'; -import IconMegaphone from 'toolbar/components/icon/IconMegaphone'; -import IconOpen from 'toolbar/components/icon/IconOpen'; -import IconPin from 'toolbar/components/icon/IconPin'; -import IconSentry from 'toolbar/components/icon/IconSentry'; -import IconSettings from 'toolbar/components/icon/IconSettings'; -import {useApiProxyInstance} from 'toolbar/context/ApiProxyContext'; -import {useConfigContext} from 'toolbar/context/ConfigContext'; -import {useFeatureFlagAdapterContext} from 'toolbar/context/FeatureFlagAdapterContext'; -import {useHiddenAppContext} from 'toolbar/context/HiddenAppContext'; -import {useMousePositionContext} from 'toolbar/context/MousePositionContext'; -import useNavigationExpansion from 'toolbar/hooks/useNavigationExpansion'; -import {DebugTarget} from 'toolbar/types/Configuration'; -import parsePlacement from 'toolbar/utils/parsePlacement'; - -const navClassName = cva('flex items-center gap-1', { - variants: { - isHorizontal: { - false: ['flex-col'], - true: ['flex-row'], - }, - }, -}); - -const navGrabber = cva( - 'relative cursor-grab border-transparent after:absolute after:bg-translucentGray-200 after:hover:bg-gray-300', - { - variants: { - isHorizontal: { - false: ['-my-1 w-full py-1 after:top-1/2 after:h-px after:w-full'], - true: ['-mx-1 h-[34px] w-px px-1 after:left-1/2 after:h-full after:w-px'], - }, - }, - } -); - -const menuSeparator = cx('mx-1 my-0.5'); - -const navItemClassName = cx([ - 'relative', - 'flex', - 'flex-col', - 'rounded-md', - 'p-1', - 'text-gray-400', - 'border', - 'border-solid', - 'border-transparent', - 'outline-none', - 'hover:text-purple-400', - 'hover:bg-purple-100', - 'hover:border-current', - 'hover:disabled:border-transparent', - 'aria-currentPage:text-purple-400', - 'aria-currentPage:bg-purple-100', - 'aria-currentPage:border-current', -]); - -const menuItemClass = cx('flex grow gap-1 whitespace-nowrap'); - -export default function Navigation() { - const [{placement}] = useConfigContext(); - const [mousePosition] = useMousePositionContext(); - const isMoving = Boolean(mousePosition); - const {isExpanded, isPinned, setIsHovered, setIsPinned} = useNavigationExpansion(); - const {pathname} = useLocation(); - const navigate = useNavigate(); - - const {overrides} = useFeatureFlagAdapterContext(); - - const toPathOrHome = (to: To) => ({ - to, - onClick: (e: MouseEvent) => { - if (pathname === to) { - e.preventDefault(); - navigate('/'); - } - }, - }); - - const [major] = parsePlacement(placement); - const isHorizontal = ['top', 'bottom'].includes(major); - - return ( -
    setIsHovered(true)} - onMouseOut={() => setIsHovered(false)}> - - - -
    -
    - - - - - - - - Issues - - - - - - - - - User Feedback - - - - - - {Object.keys(overrides).length ? : null} - - - - Feature Flags - -
    -
    -
    - ); -} - -const optionsMenuTriggerPlacement: Record[0], Placement> = { - top: 'bottom-end', - bottom: 'top-end', - left: 'right-start', - right: 'left-start', -}; - -function OptionsMenu({isPinned, setIsPinned}: {isPinned: boolean; setIsPinned: (isPinned: boolean) => void}) { - const [{debug, placement}] = useConfigContext(); - const {pathname} = useLocation(); - const navigate = useNavigate(); - const apiProxy = useApiProxyInstance(); - const [, setIsHidden] = useHiddenAppContext(); - - const [major] = parsePlacement(placement); - - return ( - - - } - placement={optionsMenuTriggerPlacement[major]}> - {debug.includes(DebugTarget.SETTINGS) ? ( - - navigate(pathname === '/settings' ? '/' : '/settings')}> - - Init Config - -
    -
    - ) : null} - - - - setIsPinned(!isPinned)}> - - {isPinned ? 'Pinned' : 'Un-Pinned'} - - - - {isPinned ? 'This panel will stay expanded' : 'This panel will shrink when not in use'} - - - - - - - - - Help - - - - Read the docs - - - - - setIsHidden(true)}> - - Hide Toolbar - - - - Hide the toolbar for the session. -
    - Open a new tab to see it again. -
    -
    - -
    - - apiProxy.logout()}> - - Logout - -
    -
    - More options -
    - ); -} diff --git a/src/lib/components/avatar/AvatarIcon.tsx b/src/lib/components/avatar/AvatarIcon.tsx new file mode 100644 index 0000000..2f7663f --- /dev/null +++ b/src/lib/components/avatar/AvatarIcon.tsx @@ -0,0 +1,89 @@ +import {cva, cx} from 'cva'; +import {Tooltip, TooltipTrigger, TooltipContent} from 'toolbar/components/base/tooltip/Tooltip'; + +const imageClassName = cva('flex items-center justify-center truncate object-cover text-white', { + variants: { + size: { + sm: 'size-2 text-[8px]', + md: 'size-3 text-[8px]', + lg: 'size-4 text-[8px]', + }, + type: { + user: 'rounded-full', + team: 'rounded-[3px]', + }, + }, +}); + +interface Props { + name: string; + avatarUrl?: string; + type: 'user' | 'team'; + tooltip: string; + size?: 'sm' | 'md' | 'lg'; +} + +/** + * Avatars represent Orgs, People, & Teams in Sentry. + */ +export default function AvatarIcon({name, avatarUrl, type, tooltip, size = 'sm'}: Props) { + return ( + + + {avatarUrl ? ( + + ) : ( + + {getAvatarInitials(name)} + + )} + + {tooltip} + + ); +} + +const COLORS = [ + '#4674ca', // blue + '#315cac', // blue_dark + '#57be8c', // green + '#3fa372', // green_dark + '#f9a66d', // yellow_orange + '#ec5e44', // red + '#e63717', // red_dark + '#f868bc', // pink + '#6c5fc7', // purple + '#4e3fb4', // purple_dark + '#57b1be', // teal + '#847a8c', // gray +] as const; + +type Color = (typeof COLORS)[number]; + +function hashIdentifier(identifier: string) { + identifier += ''; + let hash = 0; + for (let i = 0; i < identifier.length; i++) { + hash += identifier.charCodeAt(i); + } + return hash; +} + +function getAvatarColor(identifier: string | undefined): Color { + if (identifier === undefined) { + return '#847a8c' as Color; + } + + const id = hashIdentifier(identifier); + return COLORS[id % COLORS.length] || '#847a8c'; +} + +function getAvatarInitials(displayName: string | undefined) { + const names = (typeof displayName === 'string' && displayName.trim() ? displayName : '?').split(/[-\s]+/); + + let initials = names.at(0)?.charAt(0); + if (names.length > 1 && initials) { + initials += names[names.length - 1]?.charAt(0); + } + return initials?.toUpperCase() || '?'; +} diff --git a/src/lib/components/base/Breadcrumbs.tsx b/src/lib/components/base/Breadcrumbs.tsx new file mode 100644 index 0000000..00166e1 --- /dev/null +++ b/src/lib/components/base/Breadcrumbs.tsx @@ -0,0 +1,33 @@ +import {Fragment, type ReactNode} from 'react'; +import InternalLink from 'toolbar/components/base/InternalLink'; +import IconSlashForward from 'toolbar/components/icon/IconSlashForward'; + +interface TextItem { + label: ReactNode; +} + +interface LinkItem { + label: ReactNode; + to: string; +} + +export type BreadcrumbItem = TextItem | LinkItem; + +interface Props { + items: BreadcrumbItem[]; +} + +export default function Breadcrumbs({items}: Props) { + return ( + + ); +} diff --git a/src/lib/components/base/Button.stories.tsx b/src/lib/components/base/Button.stories.tsx new file mode 100644 index 0000000..1cdf5d2 --- /dev/null +++ b/src/lib/components/base/Button.stories.tsx @@ -0,0 +1,52 @@ +import type {Meta, StoryObj} from '@storybook/react-vite'; +import Button from 'toolbar/components/base/Button'; + +const meta: Meta = { + title: 'components/base/Button', + component: Button, + argTypes: { + variant: { + control: 'select', + options: ['default', 'primary'], + }, + children: { + control: 'text', + }, + disabled: { + control: 'boolean', + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + children: 'Click me', + variant: 'default', + }, +}; + +export const Primary: Story = { + args: { + children: 'Click me', + variant: 'primary', + }, +}; + +export const Disabled: Story = { + args: { + children: 'Disabled', + variant: 'default', + disabled: true, + }, +}; + +export const PrimaryDisabled: Story = { + args: { + children: 'Disabled', + variant: 'primary', + disabled: true, + }, +}; diff --git a/src/lib/components/base/Button.tsx b/src/lib/components/base/Button.tsx new file mode 100644 index 0000000..ee7f8eb --- /dev/null +++ b/src/lib/components/base/Button.tsx @@ -0,0 +1,30 @@ +import {cva, cx} from 'cva'; +import type {ComponentProps} from 'react'; + +const buttonVariants = cva( + 'flex cursor-pointer items-center justify-center gap-1 rounded-md border px-0.75 text-sm disabled:cursor-default disabled:opacity-50', + { + variants: { + variant: { + default: 'border-gray-200 hover:bg-gray-100 disabled:hover:bg-transparent', + primary: + 'border-purple-300 bg-purple-300 text-white hover:border-purple-400 hover:bg-purple-400 disabled:hover:border-purple-300', + }, + }, + defaultVariants: { + variant: 'default', + }, + } +); + +type Props = ComponentProps<'button'> & { + variant?: 'default' | 'primary'; +}; + +export default function Button({className, children, variant, ...props}: Props) { + return ( + + ); +} diff --git a/src/lib/components/base/InternalLink.tsx b/src/lib/components/base/InternalLink.tsx new file mode 100644 index 0000000..2d63721 --- /dev/null +++ b/src/lib/components/base/InternalLink.tsx @@ -0,0 +1,20 @@ +import {cx} from 'cva'; +import type {ComponentProps} from 'react'; +import {Link} from 'react-router-dom'; +import {twMerge} from 'tailwind-merge'; + +interface Props extends Omit, 'to'> { + children: React.ReactNode; + to: string; + className?: string; +} + +const linkClass = cx('text-blue-400 hover:underline'); + +export default function InternalLink({children, className, to, ...props}: Props) { + return ( + + {children} + + ); +} diff --git a/src/lib/components/LoadingSpinner.stories.tsx b/src/lib/components/base/LoadingSpinner.stories.tsx similarity index 90% rename from src/lib/components/LoadingSpinner.stories.tsx rename to src/lib/components/base/LoadingSpinner.stories.tsx index a0da229..78d3671 100644 --- a/src/lib/components/LoadingSpinner.stories.tsx +++ b/src/lib/components/base/LoadingSpinner.stories.tsx @@ -1,5 +1,5 @@ import type {Meta, StoryObj} from '@storybook/react-vite'; -import LoadingSpinner from 'toolbar/components/LoadingSpinner'; +import LoadingSpinner from 'toolbar/components/base/LoadingSpinner'; const meta: Meta = { title: 'Components/LoadingSpinner', diff --git a/src/lib/components/LoadingSpinner.tsx b/src/lib/components/base/LoadingSpinner.tsx similarity index 100% rename from src/lib/components/LoadingSpinner.tsx rename to src/lib/components/base/LoadingSpinner.tsx diff --git a/src/lib/components/base/Placeholder.stories.tsx b/src/lib/components/base/Placeholder.stories.tsx new file mode 100644 index 0000000..23ae13a --- /dev/null +++ b/src/lib/components/base/Placeholder.stories.tsx @@ -0,0 +1,227 @@ +import type {Meta, StoryObj} from '@storybook/react-vite'; +import type {VariantProps} from 'cva'; +import Placeholder from 'toolbar/components/base/Placeholder'; + +type PlaceholderProps = VariantProps & { + children?: React.ReactNode; +}; + +function PlaceholderDemo({children, ...props}: PlaceholderProps) { + return
    {children}
    ; +} + +const meta: Meta = { + title: 'components/base/Placeholder', + component: PlaceholderDemo, + argTypes: { + height: { + control: 'select', + options: ['full', 'auto', 'text', 'small', 'medium'], + }, + width: { + control: 'select', + options: ['full', 'auto'], + }, + shape: { + control: 'select', + options: ['square', 'round'], + }, + state: { + control: 'select', + options: ['normal', 'error'], + }, + children: { + control: 'text', + }, + }, + decorators: [ + Story => ( +
    + +
    + ), + ], +}; + +export default meta; +type Story = StoryObj; + +// Playground with all controls +export const Playground: Story = { + args: { + height: 'medium', + width: 'full', + shape: 'square', + state: 'normal', + }, +}; + +// Height variants +export const HeightText: Story = { + args: { + height: 'text', + width: 'full', + }, +}; + +export const HeightSmall: Story = { + args: { + height: 'small', + width: 'full', + }, +}; + +export const HeightMedium: Story = { + args: { + height: 'medium', + width: 'full', + }, +}; + +export const HeightFull: Story = { + args: { + height: 'full', + width: 'full', + }, +}; + +export const HeightAuto: Story = { + args: { + height: 'auto', + width: 'full', + children: 'Content determines height', + }, +}; + +// Width variants +export const WidthFull: Story = { + args: { + height: 'medium', + width: 'full', + }, +}; + +export const WidthAuto: Story = { + args: { + height: 'medium', + width: 'auto', + children: 'Auto width', + }, +}; + +// Shape variants +export const ShapeSquare: Story = { + args: { + height: 'medium', + width: 'auto', + shape: 'square', + }, + decorators: [ + Story => ( +
    + +
    + ), + ], +}; + +export const ShapeRound: Story = { + args: { + height: 'full', + width: 'auto', + shape: 'round', + }, + decorators: [ + Story => ( +
    + +
    + ), + ], +}; + +// State variants +export const StateNormal: Story = { + args: { + height: 'medium', + width: 'full', + state: 'normal', + }, +}; + +export const StateError: Story = { + args: { + height: 'medium', + width: 'full', + state: 'error', + children: 'Error state', + }, +}; + +// Combined examples +export const RoundAvatar: Story = { + name: 'Avatar Placeholder', + args: { + height: 'full', + width: 'auto', + shape: 'round', + state: 'normal', + }, + decorators: [ + Story => ( +
    + +
    + ), + ], +}; + +export const TextLinePlaceholder: Story = { + name: 'Text Line Placeholder', + args: { + height: 'text', + width: 'full', + shape: 'square', + state: 'normal', + }, +}; + +export const CardPlaceholder: Story = { + name: 'Card Placeholder', + args: { + height: 'full', + width: 'full', + shape: 'square', + state: 'normal', + }, +}; + +export const ErrorCardPlaceholder: Story = { + name: 'Error Card Placeholder', + args: { + height: 'full', + width: 'full', + shape: 'square', + state: 'error', + children: 'Something went wrong', + }, +}; + +// Multiple placeholders to simulate a loading skeleton +export const SkeletonExample: Story = { + name: 'Skeleton Loading Example', + render: () => ( +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + ), +}; diff --git a/src/lib/components/Placeholder.tsx b/src/lib/components/base/Placeholder.tsx similarity index 53% rename from src/lib/components/Placeholder.tsx rename to src/lib/components/base/Placeholder.tsx index ebf03d4..3f038f5 100644 --- a/src/lib/components/Placeholder.tsx +++ b/src/lib/components/base/Placeholder.tsx @@ -2,20 +2,33 @@ import {cva} from 'cva'; const Placeholder = cva('flex shrink-0 flex-col items-center justify-center rounded-md', { variants: { - base: { - true: 'h-14 w-full', + height: { + full: 'h-full', + auto: 'h-auto', + text: 'h-[1rem]', + small: 'h-2', + medium: 'h-3', + large: 'h-4', + }, + width: { + full: 'w-full', + auto: 'w-auto', + small: 'w-2', + medium: 'w-3', + large: 'w-4', }, shape: { square: 'rounded-md', round: 'rounded-full', }, state: { - normal: 'bg-surface-100', + normal: 'bg-gray-100', error: 'bg-red-100 text-red-200', }, }, defaultVariants: { - base: true, + height: 'text', + width: 'full', shape: 'square', state: 'normal', }, diff --git a/src/lib/components/base/ScrollableList.tsx b/src/lib/components/base/ScrollableList.tsx new file mode 100644 index 0000000..20479b4 --- /dev/null +++ b/src/lib/components/base/ScrollableList.tsx @@ -0,0 +1,21 @@ +import {cx} from 'cva'; +import type {CSSProperties} from 'react'; + +interface Props { + children: React.ReactNode; + ref?: React.RefObject | undefined; + height?: number | undefined; + transform?: CSSProperties['transform'] | undefined; +} + +export default function ScrollableList({children, ref, height, transform}: Props) { + return ( +
    +
    +
      + {children} +
    +
    +
    + ); +} diff --git a/src/lib/components/SentryAppLink.tsx b/src/lib/components/base/SentryAppLink.tsx similarity index 100% rename from src/lib/components/SentryAppLink.tsx rename to src/lib/components/base/SentryAppLink.tsx diff --git a/src/lib/components/icon/IconSlashForward.tsx b/src/lib/components/icon/IconSlashForward.tsx new file mode 100644 index 0000000..cdcb7d7 --- /dev/null +++ b/src/lib/components/icon/IconSlashForward.tsx @@ -0,0 +1,10 @@ +import type {SVGIconProps} from 'toolbar/components/icon/SVGIconBase'; +import SVGIconBase from 'toolbar/components/icon/SVGIconBase'; + +export default function IconSlashForward(props: SVGIconProps) { + return ( + + + + ); +} diff --git a/src/lib/components/layouts/CenterLayout.tsx b/src/lib/components/layouts/CenterLayout.tsx deleted file mode 100644 index 1623e69..0000000 --- a/src/lib/components/layouts/CenterLayout.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import type {ReactNode} from 'react'; - -interface Props { - children: ReactNode; -} - -export default function CenterLayout({children}: Props) { - return ( -
    {children}
    - ); -} - -CenterLayout.MainArea = function MainArea({children}: Props) { - return ( -
    - {children} -
    - ); -}; diff --git a/src/lib/components/layouts/EdgeLayout.stories.tsx b/src/lib/components/layouts/EdgeLayout.stories.tsx index 0a346c4..678efff 100644 --- a/src/lib/components/layouts/EdgeLayout.stories.tsx +++ b/src/lib/components/layouts/EdgeLayout.stories.tsx @@ -1,6 +1,6 @@ import type {Meta} from '@storybook/react-vite'; -import EdgeLayout, {MainArea, NavArea} from 'toolbar/components/layouts/EdgeLayout'; -import Navigation from 'toolbar/components/Navigation'; +import EdgeLayout, {PanelArea, NavArea} from 'toolbar/components/layouts/EdgeLayout'; +import NavigationPanel from 'toolbar/components/panels/nav/NavigationPanel'; import {StaticConfigProvider, useConfigContext} from 'toolbar/context/ConfigContext'; import type {Configuration} from 'toolbar/types/Configuration'; @@ -54,9 +54,9 @@ const Template = ({placement}: {placement: Configuration['placement']}) => { - + - Empty Panel + Empty Panel ); diff --git a/src/lib/components/layouts/EdgeLayout.tsx b/src/lib/components/layouts/EdgeLayout.tsx index f89a564..ac607fe 100644 --- a/src/lib/components/layouts/EdgeLayout.tsx +++ b/src/lib/components/layouts/EdgeLayout.tsx @@ -1,7 +1,6 @@ import {cva, cx} from 'cva'; import type {ReactNode} from 'react'; import {Fragment} from 'react/jsx-runtime'; -import DragDropPositionSurface from 'toolbar/components/DragDropPositionSurface'; import {useConfigContext} from 'toolbar/context/ConfigContext'; import type {Configuration} from 'toolbar/types/Configuration'; @@ -47,7 +46,7 @@ const navAreaClass = cva('pointer-events-auto contain-layout [grid-area:nav]', { }, }); -const mainAreaClass = cva('pointer-events-auto contain-layout [grid-area:main]', { +const panelAreaClass = cva('pointer-events-auto contain-layout [grid-area:main]', { variants: { placement: { 'top-left-corner': ['justify-self-start', 'self-start'], @@ -72,7 +71,6 @@ export default function EdgeLayout({children}: Props) { return (
    {children}
    -
    ); } @@ -91,10 +89,10 @@ export function NavArea({children}: Props) { ); } -export function MainArea({children}: Props) { +export function PanelArea({children}: Props) { const [{placement}] = useConfigContext(); return ( -
    +
    {children}
    ); diff --git a/src/lib/components/layouts/UnauthLayout.tsx b/src/lib/components/layouts/UnauthLayout.tsx deleted file mode 100644 index 46bc18d..0000000 --- a/src/lib/components/layouts/UnauthLayout.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import type {ReactNode} from 'react'; -import {useEffect, useContext, useCallback, useRef} from 'react'; -import {Tooltip, TooltipContent, TooltipTrigger} from 'toolbar/components/base/tooltip/Tooltip'; -import IconSentry from 'toolbar/components/icon/IconSentry'; -import CenterLayout from 'toolbar/components/layouts/CenterLayout'; -import UnauthPill from 'toolbar/components/unauth/UnauthPill'; -import ShadowRootContext from 'toolbar/context/ShadowRootContext'; -import useClicksOutside from 'toolbar/hooks/useClicksOutside'; -import {useSessionStorage} from 'toolbar/hooks/useStorage'; -import useTimeout from 'toolbar/hooks/useTimeout'; - -interface Props { - children: ReactNode; -} - -export default function UnauthLayout({children}: Props) { - const shadowRoot = useContext(ShadowRootContext); - const [isCollapsed, setIsCollapsed] = useSessionStorage('isUnauthCollapsed', false); - - const {start, cancel} = useTimeout({ - timeMs: 5_000, - onTimeout: useCallback(() => setIsCollapsed(true), [setIsCollapsed]), - }); - useEffect(start, [start]); - - const clicksRef = useRef(0); - useClicksOutside({ - node: shadowRoot.host, - onClickInside: useCallback(() => { - clicksRef.current = 0; - cancel(); - setIsCollapsed(false); - }, [cancel, setIsCollapsed]), - onClickOutside: useCallback(() => { - clicksRef.current++; - if (clicksRef.current >= 3) { - clicksRef.current = 0; - setIsCollapsed(true); - } - }, [setIsCollapsed]), - }); - - if (isCollapsed) { - return ( -
    - - - - - Expand Sentry Toolbar and Login - -
    - ); - } - - return ( - - - {children} - - - ); -} diff --git a/src/lib/components/panels/featureFlags/FeatureFlagsPanel.tsx b/src/lib/components/panels/featureFlags/FeatureFlagsPanel.tsx index 249d2bf..8c72126 100644 --- a/src/lib/components/panels/featureFlags/FeatureFlagsPanel.tsx +++ b/src/lib/components/panels/featureFlags/FeatureFlagsPanel.tsx @@ -1,5 +1,6 @@ import {cx} from 'cva'; import {useMemo, useState} from 'react'; +import Button from 'toolbar/components/base/Button'; import ExternalLink from 'toolbar/components/base/ExternalLink'; import IconChevron from 'toolbar/components/icon/IconChevron'; import IconClose from 'toolbar/components/icon/IconClose'; @@ -111,12 +112,10 @@ function FeatureFlagEditor() { {prefilter === 'overrides' ? (
    - +
    ) : null}
    diff --git a/src/lib/components/panels/feedback/FeedbackListItem.tsx b/src/lib/components/panels/feedback/FeedbackListItem.tsx index b1fb33e..04c4b30 100644 --- a/src/lib/components/panels/feedback/FeedbackListItem.tsx +++ b/src/lib/components/panels/feedback/FeedbackListItem.tsx @@ -2,6 +2,7 @@ import {cx} from 'cva'; import type {ReactElement} from 'react'; import {useMemo} from 'react'; import AssignedTo from 'toolbar/components/AssignedTo'; +import SentryAppLink from 'toolbar/components/base/SentryAppLink'; import {Tooltip, TooltipTrigger, TooltipContent} from 'toolbar/components/base/tooltip/Tooltip'; import RelativeDateTime from 'toolbar/components/datetime/RelativeDateTime'; import IconChat from 'toolbar/components/icon/IconChat'; @@ -9,7 +10,6 @@ import IconFatal from 'toolbar/components/icon/IconFatal'; import IconImage from 'toolbar/components/icon/IconImage'; import IconPlay from 'toolbar/components/icon/IconPlay'; import ProjectIcon from 'toolbar/components/project/ProjectIcon'; -import SentryAppLink from 'toolbar/components/SentryAppLink'; import {useConfigContext} from 'toolbar/context/ConfigContext'; import useReplayCount from 'toolbar/hooks/useReplayCount'; import type {FeedbackIssueListItem} from 'toolbar/sentryApi/types/group'; diff --git a/src/lib/components/panels/feedback/FeedbackPanel.tsx b/src/lib/components/panels/feedback/FeedbackPanel.tsx index d1faddb..fa9aac3 100644 --- a/src/lib/components/panels/feedback/FeedbackPanel.tsx +++ b/src/lib/components/panels/feedback/FeedbackPanel.tsx @@ -1,11 +1,12 @@ +import {cx} from 'cva'; +import Placeholder from 'toolbar/components/base/Placeholder'; +import SentryAppLink from 'toolbar/components/base/SentryAppLink'; import {Tooltip, TooltipContent, TooltipTrigger} from 'toolbar/components/base/tooltip/Tooltip'; import InfiniteListItems from 'toolbar/components/InfiniteListItems'; import InfiniteListState from 'toolbar/components/InfiniteListState'; import FeedbackListItem from 'toolbar/components/panels/feedback/FeedbackListItem'; import useInfiniteFeedbackList from 'toolbar/components/panels/feedback/useInfiniteFeedbackList'; -import Placeholder from 'toolbar/components/Placeholder'; import ProjectIcon from 'toolbar/components/project/ProjectIcon'; -import SentryAppLink from 'toolbar/components/SentryAppLink'; import {useConfigContext} from 'toolbar/context/ConfigContext'; import useFetchSentryData from 'toolbar/hooks/fetch/useFetchSentryData'; import useCurrentSentryTransactionName from 'toolbar/hooks/useCurrentSentryTransactionName'; @@ -54,10 +55,10 @@ export default function FeedbackPanel() { backgroundUpdatingMessage={() => null} loadingMessage={() => (
    -
    -
    -
    -
    +
    +
    +
    +
    )}> > diff --git a/src/lib/components/panels/issues/IssueListItem.tsx b/src/lib/components/panels/issues/IssueListItem.tsx index 9687208..dcf702f 100644 --- a/src/lib/components/panels/issues/IssueListItem.tsx +++ b/src/lib/components/panels/issues/IssueListItem.tsx @@ -1,8 +1,8 @@ import {cx} from 'cva'; import AssignedTo from 'toolbar/components/AssignedTo'; +import SentryAppLink from 'toolbar/components/base/SentryAppLink'; import RelativeDateTime from 'toolbar/components/datetime/RelativeDateTime'; import ProjectIcon from 'toolbar/components/project/ProjectIcon'; -import SentryAppLink from 'toolbar/components/SentryAppLink'; import {useConfigContext} from 'toolbar/context/ConfigContext'; import type {Group} from 'toolbar/sentryApi/types/group'; import type Member from 'toolbar/sentryApi/types/Member'; diff --git a/src/lib/components/panels/issues/IssuesPanel.tsx b/src/lib/components/panels/issues/IssuesPanel.tsx index 3159e41..0feddcc 100644 --- a/src/lib/components/panels/issues/IssuesPanel.tsx +++ b/src/lib/components/panels/issues/IssuesPanel.tsx @@ -1,11 +1,12 @@ +import {cx} from 'cva'; +import Placeholder from 'toolbar/components/base/Placeholder'; +import SentryAppLink from 'toolbar/components/base/SentryAppLink'; import {Tooltip, TooltipContent, TooltipTrigger} from 'toolbar/components/base/tooltip/Tooltip'; import InfiniteListItems from 'toolbar/components/InfiniteListItems'; import InfiniteListState from 'toolbar/components/InfiniteListState'; import IssueListItem from 'toolbar/components/panels/issues/IssueListItem'; import useInfiniteIssuesList from 'toolbar/components/panels/issues/useInfiniteIssuesList'; -import Placeholder from 'toolbar/components/Placeholder'; import ProjectIcon from 'toolbar/components/project/ProjectIcon'; -import SentryAppLink from 'toolbar/components/SentryAppLink'; import {useConfigContext} from 'toolbar/context/ConfigContext'; import useFetchSentryData from 'toolbar/hooks/fetch/useFetchSentryData'; import useCurrentSentryTransactionName from 'toolbar/hooks/useCurrentSentryTransactionName'; @@ -56,10 +57,10 @@ export default function IssuesPanel() { backgroundUpdatingMessage={() => null} loadingMessage={() => (
    -
    -
    -
    -
    +
    +
    +
    +
    )}> > diff --git a/src/lib/components/Navigation.stories.tsx b/src/lib/components/panels/nav/AuthNavigation.stories.tsx similarity index 90% rename from src/lib/components/Navigation.stories.tsx rename to src/lib/components/panels/nav/AuthNavigation.stories.tsx index f44329e..06e7f21 100644 --- a/src/lib/components/Navigation.stories.tsx +++ b/src/lib/components/panels/nav/AuthNavigation.stories.tsx @@ -1,12 +1,12 @@ import type {Meta} from '@storybook/react-vite'; import EdgeLayout, {NavArea} from 'toolbar/components/layouts/EdgeLayout'; -import Navigation from 'toolbar/components/Navigation'; +import NavigationPanel from 'toolbar/components/panels/nav/NavigationPanel'; import {useConfigContext, StaticConfigProvider} from 'toolbar/context/ConfigContext'; import type {Configuration} from 'toolbar/types/Configuration'; const meta = { - title: 'components/Navigation', - component: Navigation, + title: 'components/panels/nav/NavigationPanel', + component: NavigationPanel, parameters: { // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout layout: 'centered', @@ -34,7 +34,7 @@ const meta = { ], }, }, -} as Meta; +} as Meta; export default meta; @@ -45,7 +45,7 @@ const Template = ({placement}: {placement: Configuration['placement']}) => { - + diff --git a/src/lib/components/panels/nav/NavButton.tsx b/src/lib/components/panels/nav/NavButton.tsx new file mode 100644 index 0000000..e069e2b --- /dev/null +++ b/src/lib/components/panels/nav/NavButton.tsx @@ -0,0 +1,57 @@ +import {cx} from 'cva'; +import type {MouseEvent, ReactNode} from 'react'; +import {NavLink, useLocation, useNavigate} from 'react-router-dom'; +import type {To} from 'react-router-dom'; +import {Tooltip, TooltipTrigger, TooltipContent} from 'toolbar/components/base/tooltip/Tooltip'; + +interface Props { + to: To; + tooltip: string; + children: ReactNode; +} + +const navItemClassName = cx([ + 'relative', + 'flex', + 'flex-col', + 'rounded-md', + 'p-1', + 'text-gray-400', + 'border', + 'border-solid', + 'border-transparent', + 'outline-none', + 'hover:text-purple-400', + 'hover:bg-purple-100', + 'hover:border-current', + 'hover:disabled:border-transparent', + 'aria-currentPage:text-purple-400', + 'aria-currentPage:bg-purple-100', + 'aria-currentPage:border-current', +]); + +export default function NavButton({to, tooltip, children}: Props) { + const {pathname} = useLocation(); + const navigate = useNavigate(); + + const toPathOrHome = (to: To) => ({ + to, + onClick: (e: MouseEvent) => { + if (pathname === to) { + e.preventDefault(); + navigate('/'); + } + }, + }); + + return ( + + + + {children} + + + {tooltip} + + ); +} diff --git a/src/lib/components/panels/nav/NavGrabber.tsx b/src/lib/components/panels/nav/NavGrabber.tsx new file mode 100644 index 0000000..43cc0be --- /dev/null +++ b/src/lib/components/panels/nav/NavGrabber.tsx @@ -0,0 +1,21 @@ +import {cva} from 'cva'; + +const navGrabber = cva( + 'relative cursor-grab border-transparent after:absolute after:bg-translucentGray-200 after:hover:bg-gray-300', + { + variants: { + isHorizontal: { + false: ['-my-1 w-full py-1 after:top-1/2 after:h-px after:w-full'], + true: ['-mx-1 h-[34px] w-px px-1 after:left-1/2 after:h-full after:w-px'], + }, + }, + } +); + +interface Props { + isHorizontal: boolean; +} + +export default function NavGrabber({isHorizontal}: Props) { + return
    ; +} diff --git a/src/lib/components/panels/nav/NavigationPanel.tsx b/src/lib/components/panels/nav/NavigationPanel.tsx new file mode 100644 index 0000000..3e9b2af --- /dev/null +++ b/src/lib/components/panels/nav/NavigationPanel.tsx @@ -0,0 +1,87 @@ +import {Transition} from '@headlessui/react'; +import {cva, cx} from 'cva'; +import type {MouseEvent} from 'react'; +import {useLocation, useNavigate} from 'react-router-dom'; +import type {To} from 'react-router-dom'; +import Indicator from 'toolbar/components/base/Indicator'; +import IconFlag from 'toolbar/components/icon/IconFlag'; +import IconIssues from 'toolbar/components/icon/IconIssues'; +import IconMegaphone from 'toolbar/components/icon/IconMegaphone'; +import IconSentry from 'toolbar/components/icon/IconSentry'; +import NavButton from 'toolbar/components/panels/nav/NavButton'; +import NavGrabber from 'toolbar/components/panels/nav/NavGrabber'; +import useNavigationExpansion from 'toolbar/components/panels/nav/useNavigationExpansion'; +import {useApiProxyState} from 'toolbar/context/ApiProxyContext'; +import {useConfigContext} from 'toolbar/context/ConfigContext'; +import {useFeatureFlagAdapterContext} from 'toolbar/context/FeatureFlagAdapterContext'; +import {useMousePositionContext} from 'toolbar/context/MousePositionContext'; +import parsePlacement from 'toolbar/utils/parsePlacement'; + +const navClassName = cva('flex items-center gap-1', { + variants: { + isHorizontal: { + false: ['flex-col'], + true: ['flex-row'], + }, + }, +}); + +export default function NavigationPanel() { + const [{placement}] = useConfigContext(); + const [mousePosition] = useMousePositionContext(); + const isMoving = Boolean(mousePosition); + const {isExpanded, setIsHovered} = useNavigationExpansion(); + const {pathname} = useLocation(); + const navigate = useNavigate(); + + const proxyState = useApiProxyState(); + const {overrides} = useFeatureFlagAdapterContext(); + + const toPathOrHome = (to: To) => ({ + to, + onClick: (e: MouseEvent) => { + if (pathname === to) { + e.preventDefault(); + navigate('/'); + } + }, + }); + + const [major] = parsePlacement(placement); + const isHorizontal = ['top', 'bottom'].includes(major); + + return ( +
    setIsHovered(true)} + onMouseOut={() => setIsHovered(false)}> + + + + + +
    + + + {proxyState === 'logged-in' ? ( + + + + ) : null} + + {proxyState === 'logged-in' ? ( + + + + ) : null} + + + {Object.keys(overrides).length ? : null} + + +
    +
    +
    + ); +} diff --git a/src/lib/hooks/useNavigationExpansion.ts b/src/lib/components/panels/nav/useNavigationExpansion.ts similarity index 100% rename from src/lib/hooks/useNavigationExpansion.ts rename to src/lib/components/panels/nav/useNavigationExpansion.ts diff --git a/src/lib/components/panels/settings/ConfigPanel.tsx b/src/lib/components/panels/settings/ConfigPanel.tsx new file mode 100644 index 0000000..2192270 --- /dev/null +++ b/src/lib/components/panels/settings/ConfigPanel.tsx @@ -0,0 +1,36 @@ +import {cx} from 'cva'; +import Breadcrumbs from 'toolbar/components/base/Breadcrumbs'; +import ScrollableList from 'toolbar/components/base/ScrollableList'; +import IconSettings from 'toolbar/components/icon/IconSettings'; +import {useConfigContext} from 'toolbar/context/ConfigContext'; + +const sectionPadding = cx('px-2 py-1'); +const sectionBorder = cx('border-b border-b-translucentGray-200'); + +export default function ConfigPanel() { + const [config] = useConfigContext(); + + return ( +
    +

    + + +

    + + + {Object.entries(config).map(([key, value]) => ( +
  • + {key} + + {typeof value === 'function' + ? '[Function]' + String(value) + : typeof value === 'object' && value !== null + ? JSON.stringify(value) + : String(value)} + +
  • + ))} +
    +
    + ); +} diff --git a/src/lib/components/panels/settings/CurrentUser.tsx b/src/lib/components/panels/settings/CurrentUser.tsx new file mode 100644 index 0000000..3b587da --- /dev/null +++ b/src/lib/components/panels/settings/CurrentUser.tsx @@ -0,0 +1,49 @@ +import {cx} from 'cva'; +import AvatarIcon from 'toolbar/components/avatar/AvatarIcon'; +import Placeholder from 'toolbar/components/base/Placeholder'; +import Media from 'toolbar/components/Media'; +import useFetchSentryData from 'toolbar/hooks/fetch/useFetchSentryData'; +import {useUserQuery} from 'toolbar/sentryApi/queryKeys'; + +export default function CurrentUser() { + const {data, isError, isPending} = useFetchSentryData({ + ...useUserQuery('me'), + }); + + if (isPending || isError) { + const redPlaceholderClass = Placeholder({ + height: 'text', + width: 'auto', + state: isError ? 'normal' : 'error', + }); + return ( + + } + title={
    } + description={
    } + size="lg" + /> + ); + } + + const user = data.json; + return ( + + } + title={user.name} + description={user.email} + size="lg" + /> + ); +} diff --git a/src/lib/components/panels/settings/ProxyState.tsx b/src/lib/components/panels/settings/ProxyState.tsx new file mode 100644 index 0000000..ddc1d20 --- /dev/null +++ b/src/lib/components/panels/settings/ProxyState.tsx @@ -0,0 +1,28 @@ +import Connecting from 'toolbar/components/panels/settings/proxyState/Connecting'; +import Disconnected from 'toolbar/components/panels/settings/proxyState/Disconnected'; +import InvalidDomain from 'toolbar/components/panels/settings/proxyState/InvalidDomain'; +import Login from 'toolbar/components/panels/settings/proxyState/Login'; +import Logout from 'toolbar/components/panels/settings/proxyState/Logout'; +import MissingProject from 'toolbar/components/panels/settings/proxyState/MissingProject'; +import {useApiProxyState} from 'toolbar/context/ApiProxyContext'; +import type {ProxyState} from 'toolbar/utils/ApiProxy'; + +export default function ProxyState() { + const proxyState = useApiProxyState(); + + switch (proxyState) { + case 'disconnected': + return ; + case 'connecting': + case 'stale': // Fallthrough + return ; + case 'logged-out': + return ; + case 'missing-project': + return ; + case 'invalid-domain': + return ; + case 'logged-in': + return ; + } +} diff --git a/src/lib/components/panels/settings/SettingsPanel.tsx b/src/lib/components/panels/settings/SettingsPanel.tsx index 82116d2..c4749c7 100644 --- a/src/lib/components/panels/settings/SettingsPanel.tsx +++ b/src/lib/components/panels/settings/SettingsPanel.tsx @@ -1,15 +1,79 @@ -import {useConfigContext} from 'toolbar/context/ConfigContext'; +import {cx} from 'cva'; +import {twMerge} from 'tailwind-merge'; +import Button from 'toolbar/components/base/Button'; +import ExternalLink from 'toolbar/components/base/ExternalLink'; +import InternalLink from 'toolbar/components/base/InternalLink'; +import ScrollableList from 'toolbar/components/base/ScrollableList'; +import SwitchButton from 'toolbar/components/base/SwitchButton'; +import IconChevron from 'toolbar/components/icon/IconChevron'; +import IconOpen from 'toolbar/components/icon/IconOpen'; +import IconPin from 'toolbar/components/icon/IconPin'; +import IconSettings from 'toolbar/components/icon/IconSettings'; +import IconShow from 'toolbar/components/icon/IconShow'; +import useNavigationExpansion from 'toolbar/components/panels/nav/useNavigationExpansion'; +import CurrentUser from 'toolbar/components/panels/settings/CurrentUser'; +import ProxyState from 'toolbar/components/panels/settings/ProxyState'; +import {useApiProxyState} from 'toolbar/context/ApiProxyContext'; +import {useHiddenAppContext} from 'toolbar/context/HiddenAppContext'; + +const sectionPadding = cx('px-2 py-1'); +const sectionBorder = cx('border-b border-b-translucentGray-200'); +const rowClass = cx('flex items-center justify-between gap-1'); export default function SettingsPanel() { - const [config] = useConfigContext(); + const proxyState = useApiProxyState(); + const [, setIsHidden] = useHiddenAppContext(); + const {isPinned, setIsPinned} = useNavigationExpansion(); return ( -
    -
    - -
    {JSON.stringify(config, null, '\t')}
    -
    -
    +
    +

    + + Settings +

    + + +
  • + {proxyState === 'logged-in' ? : null} + +
  • + +
  • + +
  • + +
  • + + + Hide for Session + + +
  • + +
  • + + Show Toolbar Config + + +
  • + +
  • + + + Read the Docs + +
  • +
    ); } diff --git a/src/lib/components/unauth/Connecting.stories.tsx b/src/lib/components/panels/settings/proxyState/Connecting.stories.tsx similarity index 87% rename from src/lib/components/unauth/Connecting.stories.tsx rename to src/lib/components/panels/settings/proxyState/Connecting.stories.tsx index 38ca81e..50f8f24 100644 --- a/src/lib/components/unauth/Connecting.stories.tsx +++ b/src/lib/components/panels/settings/proxyState/Connecting.stories.tsx @@ -1,5 +1,5 @@ import type {Meta, StoryObj} from '@storybook/react-vite'; -import Connecting from 'toolbar/components/unauth/Connecting'; +import Connecting from 'toolbar/components/panels/settings/proxyState/Connecting'; const meta = { title: 'components/unauth/Connecting', diff --git a/src/lib/components/unauth/Connecting.tsx b/src/lib/components/panels/settings/proxyState/Connecting.tsx similarity index 100% rename from src/lib/components/unauth/Connecting.tsx rename to src/lib/components/panels/settings/proxyState/Connecting.tsx diff --git a/src/lib/components/unauth/Disconnected.stories.tsx b/src/lib/components/panels/settings/proxyState/Disconnected.stories.tsx similarity index 86% rename from src/lib/components/unauth/Disconnected.stories.tsx rename to src/lib/components/panels/settings/proxyState/Disconnected.stories.tsx index 4a2934c..9e0cfb3 100644 --- a/src/lib/components/unauth/Disconnected.stories.tsx +++ b/src/lib/components/panels/settings/proxyState/Disconnected.stories.tsx @@ -1,5 +1,5 @@ import type {Meta, StoryObj} from '@storybook/react-vite'; -import Disconnected from 'toolbar/components/unauth/Disconnected'; +import Disconnected from 'toolbar/components/panels/settings/proxyState/Disconnected'; const meta = { title: 'components/unauth/Disconnected', diff --git a/src/lib/components/unauth/Disconnected.tsx b/src/lib/components/panels/settings/proxyState/Disconnected.tsx similarity index 100% rename from src/lib/components/unauth/Disconnected.tsx rename to src/lib/components/panels/settings/proxyState/Disconnected.tsx diff --git a/src/lib/components/unauth/InvalidDomain.stories.tsx b/src/lib/components/panels/settings/proxyState/InvalidDomain.stories.tsx similarity index 86% rename from src/lib/components/unauth/InvalidDomain.stories.tsx rename to src/lib/components/panels/settings/proxyState/InvalidDomain.stories.tsx index 0e37271..3ada5ff 100644 --- a/src/lib/components/unauth/InvalidDomain.stories.tsx +++ b/src/lib/components/panels/settings/proxyState/InvalidDomain.stories.tsx @@ -1,5 +1,5 @@ import type {Meta, StoryObj} from '@storybook/react-vite'; -import InvalidDomain from 'toolbar/components/unauth/InvalidDomain'; +import InvalidDomain from 'toolbar/components/panels/settings/proxyState/InvalidDomain'; const meta = { title: 'components/unauth/InvalidDomain', diff --git a/src/lib/components/unauth/InvalidDomain.tsx b/src/lib/components/panels/settings/proxyState/InvalidDomain.tsx similarity index 79% rename from src/lib/components/unauth/InvalidDomain.tsx rename to src/lib/components/panels/settings/proxyState/InvalidDomain.tsx index 0452d81..6f7b23a 100644 --- a/src/lib/components/unauth/InvalidDomain.tsx +++ b/src/lib/components/panels/settings/proxyState/InvalidDomain.tsx @@ -1,4 +1,4 @@ -import {UnauthPillAppLink} from 'toolbar/components/unauth/UnauthPill'; +import SentryAppLink from 'toolbar/components/base/SentryAppLink'; import {useConfigContext} from 'toolbar/context/ConfigContext'; export default function InvalidDomain() { @@ -7,7 +7,7 @@ export default function InvalidDomain() { return (
    The domain is invalid or not configured - Configure project - +
    ); } diff --git a/src/lib/components/unauth/Login.stories.tsx b/src/lib/components/panels/settings/proxyState/Login.stories.tsx similarity index 88% rename from src/lib/components/unauth/Login.stories.tsx rename to src/lib/components/panels/settings/proxyState/Login.stories.tsx index 2c5dda0..b81b081 100644 --- a/src/lib/components/unauth/Login.stories.tsx +++ b/src/lib/components/panels/settings/proxyState/Login.stories.tsx @@ -1,5 +1,5 @@ import type {Meta, StoryObj} from '@storybook/react-vite'; -import Login from 'toolbar/components/unauth/Login'; +import Login from 'toolbar/components/panels/settings/proxyState/Login'; const meta = { title: 'components/unauth/Login', diff --git a/src/lib/components/unauth/Login.tsx b/src/lib/components/panels/settings/proxyState/Login.tsx similarity index 72% rename from src/lib/components/unauth/Login.tsx rename to src/lib/components/panels/settings/proxyState/Login.tsx index 6068004..3a79fe7 100644 --- a/src/lib/components/unauth/Login.tsx +++ b/src/lib/components/panels/settings/proxyState/Login.tsx @@ -1,5 +1,6 @@ import {useCallback, useRef, useState} from 'react'; -import {UnauthPillButton} from 'toolbar/components/unauth/UnauthPill'; +import Button from 'toolbar/components/base/Button'; +import IconLock from 'toolbar/components/icon/IconLock'; import {useApiProxyInstance} from 'toolbar/context/ApiProxyContext'; import {useConfigContext} from 'toolbar/context/ConfigContext'; import {DebugTarget} from 'toolbar/types/Configuration'; @@ -28,7 +29,7 @@ export default function Login() { const openPopup = useCallback(() => { setIsLoggingIn(true); - apiProxy.login(debugLoginSuccess ? undefined : 3000); + apiProxy.login(debugLoginSuccess ? undefined : POPUP_MESSAGE_DELAY_MS); // start timer, after a sec ask about popups if (timeoutRef.current) { @@ -40,14 +41,19 @@ export default function Login() { }, [apiProxy, debugLoginSuccess]); return ( -
    +
    {isLoggingIn ? ( -
    +
    Logging in... - reset +
    ) : ( - Login to Sentry + )} {showPopupBlockerMessage ? (
    Don't see the login popup? Check your popup blocker
    diff --git a/src/lib/components/panels/settings/proxyState/Logout.tsx b/src/lib/components/panels/settings/proxyState/Logout.tsx new file mode 100644 index 0000000..61577e7 --- /dev/null +++ b/src/lib/components/panels/settings/proxyState/Logout.tsx @@ -0,0 +1,13 @@ +import Button from 'toolbar/components/base/Button'; +import IconLock from 'toolbar/components/icon/IconLock'; +import {useApiProxyInstance} from 'toolbar/context/ApiProxyContext'; + +export default function Logout() { + const apiProxy = useApiProxyInstance(); + return ( + + ); +} diff --git a/src/lib/components/unauth/MissingProject.stories.tsx b/src/lib/components/panels/settings/proxyState/MissingProject.stories.tsx similarity index 86% rename from src/lib/components/unauth/MissingProject.stories.tsx rename to src/lib/components/panels/settings/proxyState/MissingProject.stories.tsx index 52dcc5b..4f7ae72 100644 --- a/src/lib/components/unauth/MissingProject.stories.tsx +++ b/src/lib/components/panels/settings/proxyState/MissingProject.stories.tsx @@ -1,5 +1,5 @@ import type {Meta, StoryObj} from '@storybook/react-vite'; -import MissingProject from 'toolbar/components/unauth/MissingProject'; +import MissingProject from 'toolbar/components/panels/settings/proxyState/MissingProject'; const meta = { title: 'components/unauth/MissingProject', diff --git a/src/lib/components/unauth/MissingProject.tsx b/src/lib/components/panels/settings/proxyState/MissingProject.tsx similarity index 76% rename from src/lib/components/unauth/MissingProject.tsx rename to src/lib/components/panels/settings/proxyState/MissingProject.tsx index cc089d6..0cc4e9f 100644 --- a/src/lib/components/unauth/MissingProject.tsx +++ b/src/lib/components/panels/settings/proxyState/MissingProject.tsx @@ -1,4 +1,4 @@ -import {UnauthPillAppLink} from 'toolbar/components/unauth/UnauthPill'; +import SentryAppLink from 'toolbar/components/base/SentryAppLink'; import {useConfigContext} from 'toolbar/context/ConfigContext'; export default function MissingProject() { @@ -7,13 +7,13 @@ export default function MissingProject() { return (
    Missing Project - Open project list - +
    ); } diff --git a/src/lib/components/unauth/UnauthPill.tsx b/src/lib/components/unauth/UnauthPill.tsx deleted file mode 100644 index b8538ec..0000000 --- a/src/lib/components/unauth/UnauthPill.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import {cx} from 'cva'; -import {Fragment} from 'react'; -import {twMerge} from 'tailwind-merge'; -import ExternalLink from 'toolbar/components/base/ExternalLink'; -import {Menu, MenuItem} from 'toolbar/components/base/menu/Menu'; -import {Tooltip, TooltipTrigger, TooltipContent} from 'toolbar/components/base/tooltip/Tooltip'; -import IconChevron from 'toolbar/components/icon/IconChevron'; -import IconClose from 'toolbar/components/icon/IconClose'; -import IconLock from 'toolbar/components/icon/IconLock'; -import IconOpen from 'toolbar/components/icon/IconOpen'; -import IconSentry from 'toolbar/components/icon/IconSentry'; -import SentryAppLink, {type Props as SentryAppLinkProps} from 'toolbar/components/SentryAppLink'; -import {useApiProxyInstance, useApiProxyState} from 'toolbar/context/ApiProxyContext'; -import {useHiddenAppContext} from 'toolbar/context/HiddenAppContext'; - -interface Props { - children: React.ReactNode; -} - -const menuSeparator = cx('mx-1 my-0.5'); - -const buttonClass = cx( - 'rounded-full transition-all text-white-raw p-1 hover:text-black-raw hover:bg-white-raw hover:underline' -); - -const menuItemClass = cx('flex grow gap-1 whitespace-nowrap focus:bg-white-raw focus:text-black-raw'); - -export default function UnauthPill({children}: Props) { - const [, setIsHidden] = useHiddenAppContext(); - const apiProxy = useApiProxyInstance(); - const proxyState = useApiProxyState(); - - return ( -
    - - - - - - - Visit Sentry - - - {children} - - } - placement="right-start"> - - - - - - Help - - - - Read the docs - - - - - setIsHidden(true)}> - - Hide Toolbar - - - - Hide the toolbar for the session. -
    - Open a new tab to see it again. -
    -
    - - {['missing-project', 'invalid-domain', 'logged-in'].includes(proxyState) ? ( - -
    - - apiProxy.logout()}> - - Logout - -
    - ) : null} -
    -
    - ); -} - -const UnauthPillButton = function UnauthPillButton({children, ...props}: React.ComponentProps<'button'>) { - return ( - - ); -}; - -const UnauthPillAppLink = function UnauthPillAppLink({children, ...props}: SentryAppLinkProps) { - return ( - - {children} - - ); -}; - -export {UnauthPillButton, UnauthPillAppLink}; diff --git a/src/lib/context/ApiProxyContext.tsx b/src/lib/context/ApiProxyContext.tsx index b500a78..054700e 100644 --- a/src/lib/context/ApiProxyContext.tsx +++ b/src/lib/context/ApiProxyContext.tsx @@ -55,20 +55,19 @@ export function ApiProxyContextProvider({children}: Props) { const frameSrc = `${getSentryIFrameOrigin(config)}/toolbar/${organizationSlug}/${projectIdOrSlug}/iframe/?logging=${enableLogging ? '1' : ''}`; - log('Render with state', {proxyState}); return ( // eslint-disable-next-line react-hooks/refs - -