diff --git a/frontend/.eslintrc b/frontend/.eslintrc index 0f220c176e9..96de9a977da 100644 --- a/frontend/.eslintrc +++ b/frontend/.eslintrc @@ -90,7 +90,7 @@ { "name": "antd", "importNames": ["Tabs"], - "message": "Please use Tabs from /src/components/Tab instead." + "message": "Please use Tabs from @highlight-run/ui/components instead." }, { "name": "antd", diff --git a/frontend/src/__generated/index.css b/frontend/src/__generated/index.css index 0bf6b464775..503fa6066ee 100644 --- a/frontend/src/__generated/index.css +++ b/frontend/src/__generated/index.css @@ -5513,69 +5513,6 @@ body { ._13s6meo2:focus { outline: 0; } -._189mxz60 { - width: 100%; - height: 100%; - position: relative; -} -._189mxz61 { - box-shadow: none; -} -._189mxz62 { - display: flex; -} -._189mxz63 { - height: 20px; - width: 100%; - position: absolute; - top: -10px; -} -._189mxz64 { - cursor: grab; -} -._189mxz64:active { - cursor: grabbing; -} -._189mxz65 { - background-color: #e4e2e4; - height: 1px; - width: 100%; - position: relative; - top: 10px; -} -._189mxz66 { - background: none; - border-radius: 0; - border-bottom: none; - box-shadow: none; -} -._189mxz66:focus:enabled, -._189mxz66:active:enabled, -._189mxz66:hover:enabled { - background: none; - box-shadow: none; - border-radius: 0; - color: #6f6e77; -} -._189mxz67 { - color: #744ed4; -} -._189mxz68 { - color: #6f6e77; -} -._189mxz69 { - border-radius: 2px 2px 0px 0px; - height: 2px; -} -._189mxz6a { - background-color: #744ed4; -} -._189mxz6b { - background-color: var(--_1pyqka91o); -} -._189mxz6c { - background-color: #744ed4; -} .hqr510 { color: var(--_1pyqka9l); } @@ -7321,22 +7258,6 @@ body { height: 24px; align-items: center; } -._1h7r9xv0 { - height: 100%; - overflow-y: hidden; -} -.izessr0 { - padding-left: 8px; - height: 100%; -} -.izessr1:focus, -.izessr1:active, -.izessr1:hover { - background-color: #e9e8ea; -} -.izessr3 { - background-color: #eeedef; -} ._1p577ua0 { font-size: 13px; height: 100%; @@ -7368,6 +7289,18 @@ body { -webkit-line-clamp: 1; line-clamp: 1; } +.izessr0 { + padding-left: 8px; + height: 100%; +} +.izessr1:focus, +.izessr1:active, +.izessr1:hover { + background-color: #e9e8ea; +} +.izessr3 { + background-color: #eeedef; +} ._1eq64x70 { align-items: center; background-color: #ffffff; @@ -7656,30 +7589,6 @@ body { padding-top: 5.5px; padding-bottom: 5.5px; } -._1r8lbz41 { - background-color: var(--_1pyqka91k); - border: 1px solid transparent; - color: var(--_1pyqka9h); - cursor: default; -} -._1r8lbz40._1r8lbz41 { - background-color: var(--_1pyqka91l); -} -._1r8lbz42 { - border: 1px solid var(--_1pyqka9g); - border-left: 4px solid var(--_1pyqka9g); -} -._1r8lbz43 { - background-color: var(--_1pyqka9l); - color: white; -} -._1r8lbz41._1r8lbz40._1r8lbz43 { - background-color: var(--_1pyqka9n); -} -._1r8lbz44 { - height: 100%; - overflow-y: hidden; -} ._12wekn71 { width: 50%; min-width: 600px; diff --git a/frontend/src/components/Tabs/Tabs.module.css b/frontend/src/components/Tabs/Tabs.module.css deleted file mode 100644 index b535f0ff5e3..00000000000 --- a/frontend/src/components/Tabs/Tabs.module.css +++ /dev/null @@ -1,38 +0,0 @@ -.tabPane { - height: 100%; - overflow-y: auto; - - &.withPadding { - padding: var(--size-small) var(--size-medium); - } - - &.unsetOverflowY { - overflow-y: unset; - } -} - -.extraContentContainer { - align-items: center; - column-gap: var(--size-medium); - display: flex; - - &.withHeaderPadding { - padding-right: var(--size-large); - } -} - -.tabs { - font-family: var(--header-font-family) !important; - - &.noHeaderPadding { - :global(.ant-tabs-nav-wrap) { - padding: 0; - } - } - - &.border { - :global(.ant-tabs-nav) { - border-bottom: #E4E2E4 solid 1px !important; - } - } -} diff --git a/frontend/src/components/Tabs/Tabs.tsx b/frontend/src/components/Tabs/Tabs.tsx deleted file mode 100644 index 53edb1ed037..00000000000 --- a/frontend/src/components/Tabs/Tabs.tsx +++ /dev/null @@ -1,134 +0,0 @@ -// eslint-disable-next-line no-restricted-imports -import useLocalStorage from '@rehooks/local-storage' -import { isRenderable } from '@util/react' -import { Tabs as AntDesignTabs, TabsProps } from 'antd' -import clsx from 'clsx' -import React, { useEffect } from 'react' -const { TabPane } = AntDesignTabs - -import styles from './Tabs.module.css' - -export interface TabItem { - key: string - title?: string | React.ReactNode // If undefined, `key` will be used as the title - panelContent: React.ReactNode - disabled?: boolean - hidden?: boolean -} - -type Props = Pick< - TabsProps, - 'animated' | 'tabBarExtraContent' | 'centered' | 'onChange' -> & { - tabs: TabItem[] - /** A unique value to distinguish this tab with other tabs. */ - id: string - /** Whether the tab contents has the default padding. */ - noPadding?: boolean - /** Whether the tabs overflow-y should be unset */ - unsetOverflowY?: boolean - /** Whether the tab headers have the default padding. */ - noHeaderPadding?: boolean - border?: boolean - /** An HTML id to attach to the tabs. */ - tabsHtmlId?: string - className?: string - tabBarExtraContentClassName?: string - activeKeyOverride?: string -} - -const Tabs = ({ - tabs, - id, - noPadding = false, - noHeaderPadding = false, - border = false, - unsetOverflowY = false, - tabBarExtraContent, - tabsHtmlId, - className, - tabBarExtraContentClassName, - activeKeyOverride, - ...props -}: Props) => { - const [activeTab, setActiveTab] = useLocalStorage( - `tabs-${id}-active-tab`, - tabs[0].key || '0', - ) - - /** - * In cases where we render tabs conditionally, a tab may no longer be selectable because it's not rendered. - * @example We have Tab A, B, C - * On one visit, all 3 tabs are visible - * On a second visit, only Tab A and C are visible but Tab B was the last active tab. - * On the second visit, the tabs will render an empty tab because Tab B is not visible. - * In this case, we'll default to the first tab. - */ - useEffect(() => { - const activeTabIndex = tabs.findIndex((tab) => tab.key === activeTab) - - if (activeTabIndex === -1) { - setActiveTab(tabs[0].key) - } - }, [activeTab, setActiveTab, tabs]) - - const activeKey = - activeKeyOverride !== undefined ? activeKeyOverride : activeTab - - return ( - { - if (props.onChange) { - props.onChange(activeKey) - } - setActiveTab(activeKey) - }} - tabBarExtraContent={ - isRenderable(tabBarExtraContent) ? ( -
- {tabBarExtraContent} -
- ) : ( - tabBarExtraContent - ) - } - id={tabsHtmlId} - className={clsx(styles.tabs, className, { - [styles.noHeaderPadding]: noHeaderPadding, - [styles.border]: border, - })} - > - {tabs.map(({ panelContent, title, key, disabled, hidden }) => { - if (hidden) { - return null - } - return ( - - {panelContent} - - ) - })} -
- ) -} - -export default Tabs diff --git a/frontend/src/graph/generated/schemas.tsx b/frontend/src/graph/generated/schemas.tsx index ce25207a99a..d5da503cb27 100644 --- a/frontend/src/graph/generated/schemas.tsx +++ b/frontend/src/graph/generated/schemas.tsx @@ -961,6 +961,7 @@ export type Metric = { export enum MetricAggregator { Avg = 'Avg', Count = 'Count', + CountDistinct = 'CountDistinct', CountDistinctKey = 'CountDistinctKey', Max = 'Max', Min = 'Min', diff --git a/frontend/src/pages/ErrorsV2/ErrorTabContent/ErrorTabContent.tsx b/frontend/src/pages/ErrorsV2/ErrorTabContent/ErrorTabContent.tsx index f28b2c98f9c..14bf8c47e2d 100644 --- a/frontend/src/pages/ErrorsV2/ErrorTabContent/ErrorTabContent.tsx +++ b/frontend/src/pages/ErrorsV2/ErrorTabContent/ErrorTabContent.tsx @@ -1,11 +1,9 @@ -import Tabs from '@components/Tabs/Tabs' import { GetErrorGroupQuery } from '@graph/operations' import { - Badge, Box, IconSolidTerminal, IconSolidTrendingUp, - Stack, + Tabs, } from '@highlight-run/ui/components' import { ErrorInstance } from '@pages/ErrorsV2/ErrorInstance/ErrorInstance' import ErrorMetrics from '@pages/ErrorsV2/ErrorMetrics/ErrorMetrics' @@ -16,7 +14,10 @@ import { useNavigate } from 'react-router-dom' import { ErrorInstances } from '@/pages/ErrorsV2/ErrorInstances/ErrorInstances' -import styles from './ErrorTabContent.module.css' +enum ErrorTabs { + Instances = 'instances', + Metrics = 'metrics', +} type Props = React.PropsWithChildren & { errorGroup: GetErrorGroupQuery['error_group'] @@ -29,7 +30,7 @@ const ErrorTabContent: React.FC = ({ errorGroup }) => { project_id: string error_secure_id: string error_object_id?: string - error_tab_key?: 'instances' | 'metrics' + error_tab_key?: ErrorTabs }>() useHotkeys( @@ -63,70 +64,49 @@ const ErrorTabContent: React.FC = ({ errorGroup }) => { error_tab_key === 'instances' && error_object_id === undefined return ( - { - if (activeKey === 'instances') { - // we want instances to load the latest instance, not the list view - navigate(`/${project_id}/errors/${error_secure_id}`) - } else { - navigate( - `/${project_id}/errors/${error_secure_id}/${activeKey}`, - ) - } - }} - tabs={[ - { - key: 'instances', - title: ( - } - label="Instances" - shortcut="i" - /> - ), - panelContent: showAllInstances ? ( - - ) : ( - - ), - }, - { - key: 'metrics', - title: ( - } - label="Metrics" - shortcut="m" - /> - ), - panelContent: , - }, - ]} - /> - ) -} - -type TabTitleProps = { - icon: React.ReactNode - label: string - shortcut: string -} - -const TabTitle: React.FC = ({ icon, label, shortcut }) => { - return ( - - - {icon} - {label} - - + + + selectedId={error_tab_key} + onChange={(id) => { + if (id === ErrorTabs.Instances) { + // we want instances to load the latest instance, not the list view + navigate(`/${project_id}/errors/${error_secure_id}`) + } else { + navigate( + `/${project_id}/errors/${error_secure_id}/${id}`, + ) + } + }} + > + + } + badgeText="i" + > + Instances + + } + badgeText="m" + id={ErrorTabs.Metrics} + > + Metrics + + + + + {showAllInstances ? ( + + ) : ( + + )} + + + + + + ) } diff --git a/frontend/src/pages/Player/RightPlayerPanel/RightPlayerPanel.tsx b/frontend/src/pages/Player/RightPlayerPanel/RightPlayerPanel.tsx index ed08cec8996..4bfa29521ea 100644 --- a/frontend/src/pages/Player/RightPlayerPanel/RightPlayerPanel.tsx +++ b/frontend/src/pages/Player/RightPlayerPanel/RightPlayerPanel.tsx @@ -2,6 +2,7 @@ import LoadingBox from '@components/LoadingBox' import { Box } from '@highlight-run/ui/components' import { RightPanelView, + RightPlayerTab, usePlayerUIContext, } from '@pages/Player/context/PlayerUIContext' import { MetadataBox } from '@pages/Player/MetadataBox/MetadataBox' @@ -41,7 +42,7 @@ const RightPlayerPanel = () => { if (commentId) { setRightPanelView(RightPanelView.Comments) } else { - setSelectedRightPanelTab('Events') + setSelectedRightPanelTab(RightPlayerTab.Events) } }, [setRightPanelView, setSelectedRightPanelTab, setShowRightPanel]) diff --git a/frontend/src/pages/Player/RightPlayerPanel/components/NetworkResourcePanel/NetworkResourcePanel.tsx b/frontend/src/pages/Player/RightPlayerPanel/components/NetworkResourcePanel/NetworkResourcePanel.tsx index f282ad82832..65be991b0a5 100644 --- a/frontend/src/pages/Player/RightPlayerPanel/components/NetworkResourcePanel/NetworkResourcePanel.tsx +++ b/frontend/src/pages/Player/RightPlayerPanel/components/NetworkResourcePanel/NetworkResourcePanel.tsx @@ -33,8 +33,6 @@ import { useWebSocket } from '@/pages/Player/WebSocketContext/WebSocketContext' import { useSessionParams } from '@/pages/Sessions/utils' import { TraceProvider } from '@/pages/Traces/TraceProvider' -import * as styles from './NetworkResourcePanel.css' - enum NetworkRequestTabs { Info = 'Info', Errors = 'Errors', @@ -196,41 +194,6 @@ function NetworkResourceDetails({ : new Date(resource.startTime).getTime() }, [resource.startTime, resource.startTimeAbs, startTime]) - const pages = useMemo(() => { - const tabPages: any = { - [NetworkRequestTabs.Info]: { - page: ( - - ), - }, - [NetworkRequestTabs.Errors]: { - page: , - }, - [NetworkRequestTabs.Logs]: { - page: ( - - ), - }, - } - - if (isNetworkRequest) { - tabPages[NetworkRequestTabs.Trace] = { - page: , - } - } - - return tabPages - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isNetworkRequest, resource.id]) - useHotkeys( 'h', () => { @@ -345,15 +308,43 @@ function NetworkResourceDetails({ - - tab={activeTab} - setTab={(tab) => setActiveTab(tab)} - pages={pages} - noHandle - containerClass={styles.container} - tabsContainerClass={styles.tabsContainer} - pageContainerClass={styles.pageContainer} - /> + { + setActiveTab(id as NetworkRequestTabs) + }} + > + + Info + Errors + Logs + {isNetworkRequest && ( + Trace + )} + + + + + + + + + + + {isNetworkRequest && ( + + + + )} + ) } @@ -537,35 +528,30 @@ function WebSocketDetails({ - - tab={activeTab} - setTab={(tab) => setActiveTab(tab)} - pages={{ - [WebSocketTabs.Headers]: { - page: ( - - ), - }, - [WebSocketTabs.Messages]: { - page: ( - - ), - }, - }} - noHandle - tabsContainerClass={styles.tabsContainer} - pageContainerClass={styles.pageContainer} - /> + setActiveTab(id as WebSocketTabs)} + > + + Headers + Messages + + + + + + + + ) } diff --git a/frontend/src/pages/Player/RightPlayerPanel/components/Tabs/index.tsx b/frontend/src/pages/Player/RightPlayerPanel/components/Tabs/index.tsx index 69e7d04a1ec..edb115d87fd 100644 --- a/frontend/src/pages/Player/RightPlayerPanel/components/Tabs/index.tsx +++ b/frontend/src/pages/Player/RightPlayerPanel/components/Tabs/index.tsx @@ -5,15 +5,15 @@ import { IconSolidSparkles, Tabs, } from '@highlight-run/ui/components' -import EventStreamV2 from '@pages/Player/components/EventStreamV2/EventStreamV2' import { RightPlayerTab, usePlayerUIContext, } from '@pages/Player/context/PlayerUIContext' -import MetadataPanel from '@pages/Player/MetadataPanel/MetadataPanel' import { useGetWorkspaceSettingsQuery } from '@/graph/generated/hooks' import useFeatureFlag, { Feature } from '@/hooks/useFeatureFlag/useFeatureFlag' +import EventStreamV2 from '@/pages/Player/components/EventStreamV2/EventStreamV2' +import MetadataPanel from '@/pages/Player/MetadataPanel/MetadataPanel' import SessionInsights from '@/pages/Player/RightPlayerPanel/components/SessionInsights/SessionInsights' import { useApplicationContext } from '@/routers/AppRouter/context/ApplicationContext' @@ -29,55 +29,74 @@ const RightPanelTabs = () => { skip: !currentWorkspace?.id, }) + const showAiInsights = + showSessionInsights && data?.workspaceSettings?.ai_application + return ( - tab={selectedRightPanelTab} - setTab={setSelectedRightPanelTab} - pages={{ - ['Events']: { - page: , - icon: ( + selectedId={selectedRightPanelTab} + onChange={setSelectedRightPanelTab} + > + + - ), - }, - ['Metadata']: { - page: , - icon: ( + } + > + Events + + - ), - }, - ...(showSessionInsights && - data?.workspaceSettings?.ai_application - ? { - ['AI Insights']: { - page: , - icon: ( - - ), - }, - } - : {}), - }} - /> + } + > + Metadata + + {showAiInsights && ( + + } + > + AI Insights + + )} + + + + + + + + {showAiInsights && ( + + + + )} + ) } diff --git a/frontend/src/pages/Player/Toolbar/DevToolsWindowV2/DevToolsWindowV2.tsx b/frontend/src/pages/Player/Toolbar/DevToolsWindowV2/DevToolsWindowV2.tsx index 423720fe150..ddb72435351 100644 --- a/frontend/src/pages/Player/Toolbar/DevToolsWindowV2/DevToolsWindowV2.tsx +++ b/frontend/src/pages/Player/Toolbar/DevToolsWindowV2/DevToolsWindowV2.tsx @@ -16,7 +16,6 @@ import { usePlayerUIContext } from '@pages/Player/context/PlayerUIContext' import usePlayerConfiguration from '@pages/Player/PlayerHook/utils/usePlayerConfiguration' import { useReplayerContext } from '@pages/Player/ReplayerContext' import { useResourcesContext } from '@pages/Player/ResourcesContext/ResourcesContext' -import { NetworkPage } from '@pages/Player/Toolbar/DevToolsWindowV2/NetworkPage/NetworkPage' import { DEV_TOOLS_MIN_HEIGHT, ResizePanel, @@ -34,14 +33,15 @@ import React from 'react' import { useRelatedResource } from '@/components/RelatedResources/hooks' import { buildSessionParams } from '@/pages/LogsPage/utils' import { useLinkLogCursor } from '@/pages/Player/PlayerHook/utils' +import ErrorsPage from '@/pages/Player/Toolbar/DevToolsWindowV2/ErrorsPage/ErrorsPage' +import { LogLevelFilter } from '@/pages/Player/Toolbar/DevToolsWindowV2/LogLevelFilter/LogLevelFilter' import { LogSourceFilter } from '@/pages/Player/Toolbar/DevToolsWindowV2/LogSourceFilter/LogSourceFilter' +import { NetworkPage } from '@/pages/Player/Toolbar/DevToolsWindowV2/NetworkPage/NetworkPage' +import { RequestStatusFilter } from '@/pages/Player/Toolbar/DevToolsWindowV2/RequestStatusFilter/RequestStatusFilter' +import { RequestTypeFilter } from '@/pages/Player/Toolbar/DevToolsWindowV2/RequestTypeFilter/RequestTypeFilter' import { styledVerticalScrollbar } from '@/style/common.css' import { ConsolePage } from './ConsolePage/ConsolePage' -import ErrorsPage from './ErrorsPage/ErrorsPage' -import { LogLevelFilter } from './LogLevelFilter/LogLevelFilter' -import { RequestStatusFilter } from './RequestStatusFilter/RequestStatusFilter' -import { RequestTypeFilter } from './RequestTypeFilter/RequestTypeFilter' import * as styles from './style.css' const DevToolsWindowV2: React.FC< @@ -178,51 +178,25 @@ const DevToolsWindowV2: React.FC< ) : ( - - tab={selectedDevToolsTab} - setTab={(t: Tab) => { - setSelectedDevToolsTab(t) + { + setSelectedDevToolsTab(id as Tab) formStore.reset() }} - pages={{ - [Tab.Console]: { - page: ( - - ), - }, - [Tab.Errors]: { - page: ( - - ), - }, - [Tab.Network]: { - page: ( - - ), - }, - }} - right={ + > + + + Console Logs + + Errors + Network + - } - /> + + + + + + + + + + + )} )} diff --git a/frontend/src/pages/Player/context/PlayerUIContext.ts b/frontend/src/pages/Player/context/PlayerUIContext.ts index d923c4702ac..c8316a6e369 100644 --- a/frontend/src/pages/Player/context/PlayerUIContext.ts +++ b/frontend/src/pages/Player/context/PlayerUIContext.ts @@ -8,7 +8,12 @@ export enum RightPanelView { Event = 'EVENT', } -export type RightPlayerTab = 'Events' | 'Metadata' | 'AI Insights' +export enum RightPlayerTab { + Events = 'Events', + Metadata = 'Metadata', + AIInsights = 'AI Insights', +} + interface PlayerUIContext { isPlayerFullscreen: boolean setIsPlayerFullscreen: React.Dispatch> diff --git a/frontend/src/pages/ProjectSettings/ProjectSettings.module.css b/frontend/src/pages/ProjectSettings/ProjectSettings.module.css deleted file mode 100644 index ebdd2cee1e7..00000000000 --- a/frontend/src/pages/ProjectSettings/ProjectSettings.module.css +++ /dev/null @@ -1,14 +0,0 @@ -.tabsContainer { - margin-top: var(--size-large); - - /* Override some defaults we don't want on tabs. */ - :global(.ant-tabs-nav::before) { - border-bottom: 0 !important; - } - :global(.ant-tabs-tab) { - margin: 0 var(--size-large) 0 0 !important; - } - :global(.ant-tabs-content-holder) { - margin-top: var(--size-large) !important; - } -} diff --git a/frontend/src/pages/ProjectSettings/ProjectSettings.tsx b/frontend/src/pages/ProjectSettings/ProjectSettings.tsx index a052f6678e4..f6dc07bec9d 100644 --- a/frontend/src/pages/ProjectSettings/ProjectSettings.tsx +++ b/frontend/src/pages/ProjectSettings/ProjectSettings.tsx @@ -1,5 +1,4 @@ -import Tabs from '@components/Tabs/Tabs' -import { Box, Heading, Stack, Text } from '@highlight-run/ui/components' +import { Box, Heading, Stack, Tabs, Text } from '@highlight-run/ui/components' import { DangerForm } from '@pages/ProjectSettings/DangerForm/DangerForm' import { ErrorFiltersForm } from '@pages/ProjectSettings/ErrorFiltersForm/ErrorFiltersForm' import { ErrorSettingsForm } from '@pages/ProjectSettings/ErrorSettingsForm/ErrorSettingsForm' @@ -36,12 +35,23 @@ import { import { AutoresolveStaleErrorsForm } from '@/pages/ProjectSettings/AutoresolveStaleErrorsForm/AutoresolveStaleErrorsForm' import { ProjectSettingsContextProvider } from '@/pages/ProjectSettings/ProjectSettingsContext/ProjectSettingsContext' -import styles from './ProjectSettings.module.css' import { SessionFiltersCallout } from './SessionFiltersCallout/SessionFiltersCallout' +enum ProjectSettingsTabs { + General = 'general', + Sessions = 'sessions', + Errors = 'errors', + Services = 'services', + Filters = 'filters', +} + const ProjectSettings = () => { const navigate = useNavigate() - const { project_id, ...params } = useParams() + const { project_id, ...params } = useParams<{ + project_id: string + tab: ProjectSettingsTabs + [key: string]: string + }>() const [allProjectSettings, setAllProjectSettings] = useState() const { currentWorkspace } = useApplicationContext() @@ -104,7 +114,7 @@ const ProjectSettings = () => { Project Settings -
+ { loading, }} > - { - navigate(`/${project_id}/settings/${key}`) + + selectedId={ + params.tab ?? ProjectSettingsTabs.Sessions + } + onChange={(id) => { + navigate(`/${project_id}/settings/${id}`) }} - border - noHeaderPadding - noPadding - id="settingsTabs" - tabs={[ - { - key: 'general', - title: 'General', - panelContent: , - }, - { - key: 'sessions', - title: 'Session replay', - panelContent: ( - - + + + General + + + Session replay + + + Error monitoring + + + Services + + + Filters + + + + + + + + + + + Session replay + + - - - - - {workspaceSettingsData - ?.workspaceSettings - ?.enable_session_export ? ( - - ) : null} - - ), - }, - { - key: 'errors', - title: 'Error monitoring', - panelContent: ( - - + ) : ( + 'Save changes' + )} + + + + + + {workspaceSettingsData + ?.workspaceSettings + ?.enable_session_export ? ( + + ) : null} + + + + + + + Error monitoring + + - - - - - - - - - - - - - ), - }, - { - key: 'services', - title: 'Services', - panelContent: , - }, - { - key: 'filters', - title: 'Filters', - panelContent: , - }, - ]} - /> + {editProjectSettingsLoading ? ( + + ) : ( + 'Save changes' + )} + + + + + + + + + + + + + + + + + + + + + + -
+ ) diff --git a/frontend/src/pages/Traces/TraceLogs.tsx b/frontend/src/pages/Traces/TraceLogs.tsx index 5c97b00617b..862f1ad9771 100644 --- a/frontend/src/pages/Traces/TraceLogs.tsx +++ b/frontend/src/pages/Traces/TraceLogs.tsx @@ -64,12 +64,7 @@ export const TraceLogs: React.FC = () => { return ( <> - + { hideCreateAlert productType={ProductType.Logs} /> - + {(!loading && logEdges.length === 0) || !traceId ? ( - + + + + + ) : ( { - - tab={activeTab} - setTab={(tab) => setActiveTab(tab)} - containerClass={styles.tabs} - tabsContainerClass={styles.tabsContainer} - pageContainerClass={styles.tabsPageContainer} - pages={{ - [TraceTabs.Info]: { - page: ( - - - - ), - }, - [TraceTabs.Errors]: { - badge: - errors?.length > 0 ? ( - - ) : undefined, - page: , - }, - [TraceTabs.Logs]: { - page: , - }, - }} - noHandle - /> + selectedId={activeTab} onChange={setActiveTab}> + + Info + + Errors + + Logs + + + + + + + + + + + + + + + ) diff --git a/frontend/src/pages/UserSettings/UserSettings.tsx b/frontend/src/pages/UserSettings/UserSettings.tsx deleted file mode 100644 index 2f86a4118de..00000000000 --- a/frontend/src/pages/UserSettings/UserSettings.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { FieldsBox } from '@components/FieldsBox/FieldsBox' -import LeadAlignLayout from '@components/layout/LeadAlignLayout' -import Tabs from '@components/Tabs/Tabs' -import { EmailOptOutPanel } from '@pages/EmailOptOut/EmailOptOut' -import { PlayerForm } from '@pages/UserSettings/PlayerForm/PlayerForm' -import { auth } from '@util/auth' -import { useParams } from '@util/react-router/useParams' -import React from 'react' -import { Helmet } from 'react-helmet' -import { useNavigate } from 'react-router-dom' - -import commonStyles from '../../Common.module.css' -import projectSettingsStyles from '../ProjectSettings/ProjectSettings.module.css' -import Auth from './Auth/Auth' - -const UserSettings: React.FC = () => { - const navigate = useNavigate() - const params = useParams() - - const tabs = [ - ...(auth.googleProvider - ? [ - { - key: 'auth', - title: 'Authentication', - panelContent: , - }, - ] - : []), - ...[ - { - key: 'email-settings', - title: 'Email Settings', - panelContent: ( - - - - ), - }, - { - key: 'player-settings', - title: 'Player Settings', - panelContent: ( - - - - ), - }, - ], - ] - - const activeKey = params.tab ?? tabs[0].key - return ( - <> - - User Settings - - -
-
- -
-

User Settings

-
-
- { - navigate( - `${location.pathname.replace( - '/' + activeKey, - '', - )}/${key}`, - ) - }} - noHeaderPadding - noPadding - id="settingsTabs" - tabs={tabs} - /> -
-
-
-
- - ) -} - -export default UserSettings diff --git a/frontend/src/pages/WorkspaceTeam/WorkspaceTeam.tsx b/frontend/src/pages/WorkspaceTeam/WorkspaceTeam.tsx index 0c77566410e..e30fb37c621 100644 --- a/frontend/src/pages/WorkspaceTeam/WorkspaceTeam.tsx +++ b/frontend/src/pages/WorkspaceTeam/WorkspaceTeam.tsx @@ -1,12 +1,11 @@ import { Button } from '@components/Button' -import Tabs from '@components/Tabs/Tabs' import { useGetWorkspaceAdminsQuery } from '@graph/hooks' import { AdminRole, WorkspaceAdminRole } from '@graph/schemas' import { - Badge, Box, IconSolidUserAdd, Stack, + Tabs, } from '@highlight-run/ui/components' import AllMembers from '@pages/WorkspaceTeam/components/AllMembers' import { AutoJoinForm } from '@pages/WorkspaceTeam/components/AutoJoinForm' @@ -21,10 +20,13 @@ import { useToggle } from 'react-use' import layoutStyles from '../../components/layout/LeadAlignLayout.module.css' import styles from './WorkspaceTeam.module.css' -type MemberKeyType = 'members' | 'invites' +enum MemberKeyType { + Members = 'members', + Invites = 'invites', +} const WorkspaceTeam = () => { - const { workspace_id, member_tab_key = 'members' } = useParams<{ + const { workspace_id, member_tab_key = MemberKeyType.Members } = useParams<{ workspace_id: string member_tab_key?: MemberKeyType }>() @@ -59,54 +61,46 @@ const WorkspaceTeam = () => { toggleShowModal={toggleShowModal} /> - - navigate(`/w/${workspace_id}/team/${activeKey}`) - } - tabs={[ - { - key: 'members', - title: , - panelContent: ( - - - - ), - }, - { - key: 'invites', - title: , - panelContent: ( - - - - ), - }, - ]} - /> + + + selectedId={member_tab_key} + onChange={(id) => { + navigate(`/w/${workspace_id}/team/${id}`) + }} + > + + Members + + Pending invites + + + + + + + + + + + + +
) @@ -143,26 +137,4 @@ const TabContentContainer = ({ ) } -type TabTitleProps = { - label: string - count?: number -} - -const TabTitle: React.FC = ({ label, count }) => { - return ( - - - {label} - {count && ( - - )} - - - ) -} - export default WorkspaceTeam diff --git a/frontend/src/routers/ProjectRouter/ProjectRouter.tsx b/frontend/src/routers/ProjectRouter/ProjectRouter.tsx index 24a081289bd..c10660bca10 100644 --- a/frontend/src/routers/ProjectRouter/ProjectRouter.tsx +++ b/frontend/src/routers/ProjectRouter/ProjectRouter.tsx @@ -127,7 +127,7 @@ export const ProjectRouter = () => { const [selectedRightPanelTab, setSelectedRightPanelTab] = useLocalStorage( 'tabs-PlayerRightPanel-active-tab', - 'Events', + RightPlayerTab.Events, ) const { isPlayerFullscreen, setIsPlayerFullscreen, playerCenterPanelRef } = diff --git a/packages/ui/.eslintrc.json b/packages/ui/.eslintrc.json index d51980e7d9c..52ff1f40057 100644 --- a/packages/ui/.eslintrc.json +++ b/packages/ui/.eslintrc.json @@ -25,6 +25,7 @@ ], "ignorePatterns": ["src/__generated/*"], "rules": { + "@typescript-eslint/no-non-null-assertion": "off", "react/display-name": "off", "react/react-in-jsx-scope": "off", "simple-import-sort/imports": "error", diff --git a/packages/ui/package.json b/packages/ui/package.json index 94467954bcb..3bd5cd0ac43 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -15,6 +15,7 @@ "lint": "eslint ./src", "lint:ts": "tsc --noEmit", "test": "TZ=UTC vitest --run", + "test:watch": "TZ=UTC vitest", "tokens:generate": "npx ts-node-esm ./scripts/generate-tokens.ts", "typegen": "tsc --emitDeclarationOnly" }, diff --git a/packages/ui/src/__generated/ve/components/Tabs/styles.css.js b/packages/ui/src/__generated/ve/components/Tabs/styles.css.js index fa776f810e5..375f3cc644c 100644 --- a/packages/ui/src/__generated/ve/components/Tabs/styles.css.js +++ b/packages/ui/src/__generated/ve/components/Tabs/styles.css.js @@ -1,22 +1,5 @@ // ../packages/ui/src/components/Tabs/styles.css.ts -import { createRuntimeFn as _7a468 } from "@vanilla-extract/recipes/createRuntimeFn"; -var GRAB_HANDLE_HEIGHT = 20; -var controlBarBottomVariants = _7a468({ defaultClassName: "_189mxz69", variantClassNames: { selected: { true: "_189mxz6a" }, hovered: { true: "_189mxz6b" } }, defaultVariants: {}, compoundVariants: [[{ hovered: true, selected: true }, "_189mxz6c"]] }); -var controlBarButton = "_189mxz61"; -var controlBarVariants = _7a468({ defaultClassName: "_189mxz66", variantClassNames: { selected: { true: "_189mxz67", false: "_189mxz68" } }, defaultVariants: { selected: false }, compoundVariants: [] }); -var grabbable = "_189mxz64"; -var handle = "_189mxz63"; -var handleLine = "_189mxz65"; -var pageWrapper = "_189mxz60"; -var tabText = "_189mxz62"; +var tabList = "_189mxz60"; export { - GRAB_HANDLE_HEIGHT, - controlBarBottomVariants, - controlBarButton, - controlBarVariants, - grabbable, - handle, - handleLine, - pageWrapper, - tabText + tabList }; diff --git a/packages/ui/src/components/Stack/Stack.tsx b/packages/ui/src/components/Stack/Stack.tsx index 7e4ed9061ea..480cf505058 100644 --- a/packages/ui/src/components/Stack/Stack.tsx +++ b/packages/ui/src/components/Stack/Stack.tsx @@ -13,39 +13,45 @@ type Props = React.PropsWithChildren & { cssClass?: BoxProps['cssClass'] } & BoxProps -export const Stack: React.FC = ({ - as, - align, - children, - direction, - flex, - gap, - justify, - wrap, - width, - ...props -}) => { - if (typeof wrap === 'boolean') { - wrap = wrap ? 'wrap' : undefined - } +export const Stack: React.FC = React.forwardRef( + ( + { + as, + align, + children, + direction, + flex, + gap, + justify, + wrap, + width, + ...props + }, + ref, + ) => { + if (typeof wrap === 'boolean') { + wrap = wrap ? 'wrap' : undefined + } - return ( - - {children} - - ) -} + return ( + + {children} + + ) + }, +) Stack.defaultProps = { as: 'div', diff --git a/packages/ui/src/components/Tabs/Tabs.stories.tsx b/packages/ui/src/components/Tabs/Tabs.stories.tsx index 46896634a0f..089df91e4d7 100644 --- a/packages/ui/src/components/Tabs/Tabs.stories.tsx +++ b/packages/ui/src/components/Tabs/Tabs.stories.tsx @@ -1,6 +1,16 @@ import { Meta } from '@storybook/react' import { useState } from 'react' +import { Box } from '../Box/Box' +import { Button } from '../Button/Button' +import { Heading } from '../Heading/Heading' +import { + IconSolidAcademicCap, + IconSolidBeaker, + IconSolidLogs, + IconSolidTraces, +} from '../icons' +import { Stack } from '../Stack/Stack' import { Tabs } from './Tabs' export default { @@ -8,18 +18,148 @@ export default { component: Tabs, } as Meta +enum TabIds { + ONE = '1', + TWO = '2', + THREE = '3', + FOUR = '4', +} + export const Basic = () => { - const [tab, setTab] = useState('hello') + const [activeTab, setActiveTab] = useState(TabIds.ONE) + + return ( + + + + Tab 1 + }> + Tab 2 + + + Tab 3 + + } + badgeText="14" + > + Tab 4 + + + + + Panel 1{' '} + + + + + + Panel 2 + + + + + + + Panel 3 + + + + + + + Panel 4 + + + + + + ) +} + +export const Sizes = () => ( + + + Small (default) + + + + + }> + Info + + } badgeText="13"> + Logs + + } badgeText="4"> + Trace + + + + Info + + + Logs + + + Trace + + + + + + Extra Small + + + + + }> + Info + + } badgeText="13"> + Logs + + } badgeText="4"> + Trace + + + + Info + + + Logs + + + Trace + + + + +) +const TabContent: React.FC = ({ children }) => { return ( - Hi }, - there: { page:
there!
}, - world: { page:
Hello! 👋
}, - }} - /> + + {children} + ) } diff --git a/packages/ui/src/components/Tabs/Tabs.test.tsx b/packages/ui/src/components/Tabs/Tabs.test.tsx index 65dbbe0bdb1..1d2527d3f9d 100644 --- a/packages/ui/src/components/Tabs/Tabs.test.tsx +++ b/packages/ui/src/components/Tabs/Tabs.test.tsx @@ -1,18 +1,29 @@ -import { render, screen } from '@testing-library/react' +import { userEvent } from '@storybook/test' +import { render, screen, waitFor } from '@testing-library/react' import { Tabs } from './Tabs' describe('Tabs', () => { - it('exists', async () => { + it('allows you to switch between tabs', async () => { render( - Test } }} - tab="foo" - setTab={(t) => { - console.log('tab', t) - }} - />, + + + Tab 1 + Tab 2 + + Panel 1 + Panel 2 + , ) - await screen.findByText('Test') + + await waitFor(() => expect(screen.getByText('Panel 1')).toBeVisible()) + expect(screen.getByText('Panel 2')).not.toBeVisible() + + userEvent.click(screen.getByText('Tab 2')) + + await waitFor(() => + expect(screen.getByText('Panel 1')).not.toBeVisible(), + ) + waitFor(() => expect(screen.getByText('Panel 2')).toBeVisible()) }) }) diff --git a/packages/ui/src/components/Tabs/Tabs.tsx b/packages/ui/src/components/Tabs/Tabs.tsx index b1c36f74a58..8e7e93d2a58 100644 --- a/packages/ui/src/components/Tabs/Tabs.tsx +++ b/packages/ui/src/components/Tabs/Tabs.tsx @@ -1,126 +1,164 @@ -import React from 'react' +import * as Ariakit from '@ariakit/react' +import { createContext, useContext, useState } from 'react' -import { Button } from '../../components/Button/Button' -import { Text } from '../../components/Text/Text' -import { Box } from '../Box/Box' -import * as styles from './styles.css' +import { Badge } from '../Badge/Badge' +import { Box, BoxProps, PaddingProps } from '../Box/Box' +import { Stack } from '../Stack/Stack' +import { Props as TagProps, Tag } from '../Tag/Tag' -export interface Page { - page: React.ReactNode - icon?: React.ReactElement - badge?: React.ReactNode +export const TabsContext = createContext({ + size: 'sm', +}) + +type Props = React.PropsWithChildren & { + defaultSelectedId?: T + selectedId?: T + size?: 'xs' | 'sm' + onChange?: (id: T) => void +} + +export const Tabs = ({ + children, + defaultSelectedId, + selectedId, + size = 'sm', + onChange, +}: Props) => { + const tabsStore = Ariakit.useTabStore({ + defaultSelectedId, + setSelectedId: (id) => { + if (onChange) { + onChange(id as T) + } + }, + }) + + return ( + + + + {children} + + + + ) } -type Props = { - pages: { - [k: string]: Page +type TabListProps = React.PropsWithChildren & + Ariakit.TabListProps & + PaddingProps & { + gap?: BoxProps['gap'] } - tab: T - right?: React.ReactNode - setTab: (tab: T) => void - handleRef?: (ref: HTMLElement | null) => void - // These props have been added to override defaults that prevent us from - // implementing the UX as designed. They are temporary and will be removed - // when we rebuild tabs: https://github.com/highlight/highlight/issues/5771 - noHandle?: boolean - containerClass?: string - tabsContainerClass?: string - pageContainerClass?: string +const TabList: React.FC = ({ + children, + gap = '16', + ...props +}) => { + return ( + + {children} + + } + /> + ) +} + +type TabProps = Ariakit.TabProps & { + children: string + id: string + badgeText?: string + icon?: TagProps['icon'] } -export const Tabs = function ({ - pages, - tab, - right, - containerClass, - tabsContainerClass, - pageContainerClass, - noHandle = false, - setTab, - handleRef, -}: Props) { - const [hoveredTab, setHoveredTab] = React.useState() - const currentPage = pages[tab] +const Tab: React.FC = ({ badgeText, children, icon, ...props }) => { + const { size } = useContext(TabsContext) + const tabContext = Ariakit.useTabContext()! + const selected = tabContext.useState('selectedId') === props.id + const [hovered, setHovered] = useState(false) + const showBorder = hovered || selected return ( - - + setHovered(true)} + onMouseLeave={() => setHovered(false)} > - {Object.keys(pages).map((t) => ( + + {children} + + + {badgeText && ( + + )} + + {showBorder && ( setHoveredTab(t)} - onMouseLeave={() => setHoveredTab(undefined)} - onClick={() => { - setTab(t as T) + position="absolute" + backgroundColor={selected ? 'p9' : 'n7'} + borderTopLeftRadius="2" + borderTopRightRadius="2" + width="full" + style={{ + height: 2, + bottom: 0, + left: 0, + right: 0, + width: '100%', }} - > - - - - ))} - - {right} - - {currentPage && ( - - {pages[tab].page} - {!noHandle && ( - - - + /> )} - )} - + } + /> ) } + +type TabPanelProps = React.PropsWithChildren + +const TabPanel: React.FC = ({ children, ...props }) => { + return ( + + {children} + + } + /> + ) +} + +Tabs.Tab = Tab +Tabs.List = TabList +Tabs.Panel = TabPanel +Tabs.useStore = Ariakit.useTabStore +Tabs.useContext = Ariakit.useTabContext diff --git a/packages/ui/src/components/Tabs/styles.css.ts b/packages/ui/src/components/Tabs/styles.css.ts deleted file mode 100644 index a54d2b30b81..00000000000 --- a/packages/ui/src/components/Tabs/styles.css.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { style } from '@vanilla-extract/css' -import { recipe } from '@vanilla-extract/recipes' - -import { colors } from '../../css/colors' -import { themeVars } from '../../css/theme.css' - -export const GRAB_HANDLE_HEIGHT = 20 - -export const pageWrapper = style({ - width: '100%', - height: '100%', - position: 'relative', -}) - -export const controlBarButton = style({ - boxShadow: 'none', -}) - -export const tabText = style({ - display: 'flex', -}) - -export const handle = style({ - height: GRAB_HANDLE_HEIGHT, - width: '100%', - position: 'absolute', - top: -GRAB_HANDLE_HEIGHT / 2, -}) - -export const grabbable = style({ - cursor: 'grab', - selectors: { - '&:active': { - cursor: 'grabbing', - }, - }, -}) - -export const handleLine = style({ - backgroundColor: colors.n6, - height: 1, - width: '100%', - position: 'relative', - top: GRAB_HANDLE_HEIGHT / 2, -}) - -export const controlBarVariants = recipe({ - base: { - background: 'none', - borderRadius: 0, - borderBottom: `none`, - boxShadow: 'none', - selectors: { - '&:focus:enabled, &:active:enabled, &:hover:enabled': { - background: 'none', - boxShadow: 'none', - borderRadius: 0, - color: colors.n11, - }, - }, - }, - variants: { - selected: { - true: { - color: colors.p9, - }, - false: { - color: colors.n11, - }, - }, - }, - - defaultVariants: { - selected: false, - }, -}) - -export const controlBarBottomVariants = recipe({ - base: { - borderRadius: '2px 2px 0px 0px', - height: 2, - }, - variants: { - selected: { - true: { - backgroundColor: colors.p9, - }, - }, - hovered: { - true: { - backgroundColor: - themeVars.interactive.outline.secondary.enabled, - }, - }, - }, - compoundVariants: [ - { - variants: { hovered: true, selected: true }, - style: { - backgroundColor: colors.p9, - }, - }, - ], -})