Status bar initial commit
This commit is contained in:
		
							
								
								
									
										60
									
								
								src/App.tsx
									
									
									
									
									
								
							
							
						
						
									
										60
									
								
								src/App.tsx
									
									
									
									
									
								
							| @ -3,7 +3,7 @@ import { useHotKeyListener } from './hooks/useHotKeyListener' | ||||
| import { Stream } from './components/Stream' | ||||
| import { AppHeader } from './components/AppHeader' | ||||
| import { useHotkeys } from 'react-hotkeys-hook' | ||||
| import { useLoaderData, useNavigate } from 'react-router-dom' | ||||
| import { useLoaderData, useLocation, useNavigate } from 'react-router-dom' | ||||
| import { type IndexLoaderData } from 'lib/types' | ||||
| import { PATHS } from 'lib/paths' | ||||
| import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' | ||||
| @ -22,16 +22,24 @@ import Gizmo from 'components/Gizmo' | ||||
| import { CoreDumpManager } from 'lib/coredump' | ||||
| import { UnitsMenu } from 'components/UnitsMenu' | ||||
| import { CameraProjectionToggle } from 'components/CameraProjectionToggle' | ||||
| import { homeDefaultStatusBarItems } from 'components/statusBar/homeDefaultStatusBarItems' | ||||
| import { StatusBar } from 'components/StatusBar' | ||||
| import { useModelStateStatus } from 'components/ModelStateIndicator' | ||||
| import { useNetworkHealthStatus } from 'components/NetworkHealthIndicator' | ||||
| import { useModelingContext } from 'hooks/useModelingContext' | ||||
| import { xStateValueToString } from 'lib/xStateValueToString' | ||||
|  | ||||
| export function App() { | ||||
|   const { project, file } = useLoaderData() as IndexLoaderData | ||||
|   useRefreshSettings(PATHS.FILE + 'SETTINGS') | ||||
|   const navigate = useNavigate() | ||||
|   const location = useLocation() | ||||
|   const filePath = useAbsoluteFilePath() | ||||
|   const { onProjectOpen } = useLspContext() | ||||
|   // We need the ref for the outermost div so we can screenshot the app for | ||||
|   // the coredump. | ||||
|   const ref = useRef<HTMLDivElement>(null) | ||||
|   const { state: modelingState } = useModelingContext() | ||||
|  | ||||
|   const projectName = project?.name || null | ||||
|   const projectPath = project?.path || null | ||||
| @ -73,21 +81,43 @@ export function App() { | ||||
|   useEngineConnectionSubscriptions() | ||||
|  | ||||
|   return ( | ||||
|     <div className="relative h-full flex flex-col" ref={ref}> | ||||
|       <AppHeader | ||||
|         className={'transition-opacity transition-duration-75 ' + paneOpacity} | ||||
|         project={{ project, file }} | ||||
|         enableMenu={true} | ||||
|     <div className="h-screen w-full flex flex-col"> | ||||
|       <div className="relative flex flex-1 flex-col" ref={ref}> | ||||
|         <AppHeader | ||||
|           className={'transition-opacity transition-duration-75 ' + paneOpacity} | ||||
|           project={{ project, file }} | ||||
|           enableMenu={true} | ||||
|         /> | ||||
|         <ModalContainer /> | ||||
|         <ModelingSidebar paneOpacity={paneOpacity} /> | ||||
|         <Stream /> | ||||
|         {/* <CamToggle /> */} | ||||
|         <LowerRightControls coreDumpManager={coreDumpManager}> | ||||
|           <UnitsMenu /> | ||||
|           <Gizmo /> | ||||
|           <CameraProjectionToggle /> | ||||
|         </LowerRightControls> | ||||
|       </div> | ||||
|       <StatusBar | ||||
|         globalItems={[ | ||||
|           useNetworkHealthStatus(), | ||||
|           ...homeDefaultStatusBarItems({ coreDumpManager, location }), | ||||
|         ]} | ||||
|         localItems={[ | ||||
|           { | ||||
|             id: 'modeling-state', | ||||
|             element: 'text', | ||||
|             label: | ||||
|               modelingState.value instanceof Object | ||||
|                 ? xStateValueToString(modelingState.value) ?? '' | ||||
|                 : modelingState.value, | ||||
|             toolTip: { | ||||
|               children: 'The current state of the modeler', | ||||
|             }, | ||||
|           }, | ||||
|           useModelStateStatus(), | ||||
|         ]} | ||||
|       /> | ||||
|       <ModalContainer /> | ||||
|       <ModelingSidebar paneOpacity={paneOpacity} /> | ||||
|       <Stream /> | ||||
|       {/* <CamToggle /> */} | ||||
|       <LowerRightControls coreDumpManager={coreDumpManager}> | ||||
|         <UnitsMenu /> | ||||
|         <Gizmo /> | ||||
|         <CameraProjectionToggle /> | ||||
|       </LowerRightControls> | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| @ -636,6 +636,16 @@ const CustomIconMap = { | ||||
|       /> | ||||
|     </svg> | ||||
|   ), | ||||
|   loading: ( | ||||
|     <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||||
|       <path | ||||
|         fillRule="evenodd" | ||||
|         clipRule="evenodd" | ||||
|         d="M12.5001 6.25839C11.76 5.76392 10.89 5.5 10 5.5V4.5C11.0878 4.5 12.1512 4.82257 13.0556 5.42692C13.9601 6.03126 14.6651 6.89025 15.0813 7.89524C15.4976 8.90023 15.6065 10.0061 15.3943 11.073C15.1821 12.1399 14.6583 13.1199 13.8891 13.8891C13.1199 14.6583 12.1399 15.1821 11.073 15.3943C10.0061 15.6065 8.90023 15.4976 7.89524 15.0813C6.89025 14.6651 6.03126 13.9601 5.42692 13.0556C4.82257 12.1512 4.5 11.0878 4.5 10H5.5C5.5 10.89 5.76392 11.76 6.25839 12.5001C6.75285 13.2401 7.45566 13.8169 8.27792 14.1575C9.10019 14.4981 10.005 14.5872 10.8779 14.4135C11.7508 14.2399 12.5526 13.8113 13.182 13.182C13.8113 12.5526 14.2399 11.7508 14.4135 10.8779C14.5872 10.005 14.4981 9.10019 14.1575 8.27792C13.8169 7.45566 13.2401 6.75285 12.5001 6.25839Z" | ||||
|         fill="currentColor" | ||||
|       /> | ||||
|     </svg> | ||||
|   ), | ||||
|   lockClosed: ( | ||||
|     <svg | ||||
|       viewBox="0 0 20 20" | ||||
|  | ||||
| @ -1,4 +1,3 @@ | ||||
| import { APP_VERSION } from 'routes/Settings' | ||||
| import { CustomIcon } from 'components/CustomIcon' | ||||
| import Tooltip from 'components/Tooltip' | ||||
| import { PATHS } from 'lib/paths' | ||||
| @ -6,13 +5,8 @@ import { NetworkHealthIndicator } from 'components/NetworkHealthIndicator' | ||||
| import { HelpMenu } from './HelpMenu' | ||||
| import { Link, useLocation } from 'react-router-dom' | ||||
| import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' | ||||
| import { coreDump } from 'lang/wasm' | ||||
| import toast from 'react-hot-toast' | ||||
| import { CoreDumpManager } from 'lib/coredump' | ||||
| import openWindow, { openExternalBrowserIfDesktop } from 'lib/openWindow' | ||||
| import { NetworkMachineIndicator } from './NetworkMachineIndicator' | ||||
| import { ModelStateIndicator } from './ModelStateIndicator' | ||||
| import { reportRejection } from 'lib/trap' | ||||
|  | ||||
| export function LowerRightControls({ | ||||
|   children, | ||||
| @ -26,97 +20,11 @@ export function LowerRightControls({ | ||||
|   const linkOverrideClassName = | ||||
|     '!text-chalkboard-70 hover:!text-chalkboard-80 dark:!text-chalkboard-40 dark:hover:!text-chalkboard-30' | ||||
|  | ||||
|   function reportbug(event: { | ||||
|     preventDefault: () => void | ||||
|     stopPropagation: () => void | ||||
|   }) { | ||||
|     event?.preventDefault() | ||||
|     event?.stopPropagation() | ||||
|  | ||||
|     if (!coreDumpManager) { | ||||
|       // open default reporting option | ||||
|       openWindow( | ||||
|         'https://github.com/KittyCAD/modeling-app/issues/new/choose' | ||||
|       ).catch(reportRejection) | ||||
|     } else { | ||||
|       toast | ||||
|         .promise( | ||||
|           coreDump(coreDumpManager, true), | ||||
|           { | ||||
|             loading: 'Preparing bug report...', | ||||
|             success: 'Bug report opened in new window', | ||||
|             error: 'Unable to export a core dump. Using default reporting.', | ||||
|           }, | ||||
|           { | ||||
|             success: { | ||||
|               // Note: this extended duration is especially important for Playwright e2e testing | ||||
|               // default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations | ||||
|               duration: 6000, | ||||
|             }, | ||||
|           } | ||||
|         ) | ||||
|         .catch((err: Error) => { | ||||
|           if (err) { | ||||
|             openWindow( | ||||
|               'https://github.com/KittyCAD/modeling-app/issues/new/choose' | ||||
|             ).catch(reportRejection) | ||||
|           } | ||||
|         }) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <section className="fixed bottom-2 right-2 flex flex-col items-end gap-3 pointer-events-none"> | ||||
|     <section className="absolute bottom-2 right-2 flex flex-col items-end gap-3 pointer-events-none"> | ||||
|       {children} | ||||
|       <menu className="flex items-center justify-end gap-3 pointer-events-auto"> | ||||
|         {!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />} | ||||
|         <a | ||||
|           onClick={openExternalBrowserIfDesktop( | ||||
|             `https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}` | ||||
|           )} | ||||
|           href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`} | ||||
|           target="_blank" | ||||
|           rel="noopener noreferrer" | ||||
|           className={'!no-underline font-mono text-xs ' + linkOverrideClassName} | ||||
|         > | ||||
|           v{APP_VERSION} | ||||
|         </a> | ||||
|         <a | ||||
|           onClick={reportbug} | ||||
|           href="https://github.com/KittyCAD/modeling-app/issues/new/choose" | ||||
|           target="_blank" | ||||
|           rel="noopener noreferrer" | ||||
|         > | ||||
|           <CustomIcon | ||||
|             name="bug" | ||||
|             className={`w-5 h-5 ${linkOverrideClassName}`} | ||||
|           /> | ||||
|           <Tooltip position="top" contentClassName="text-xs"> | ||||
|             Report a bug | ||||
|           </Tooltip> | ||||
|         </a> | ||||
|         <Link | ||||
|           to={ | ||||
|             location.pathname.includes(PATHS.FILE) | ||||
|               ? filePath + PATHS.SETTINGS + '?tab=project' | ||||
|               : PATHS.HOME + PATHS.SETTINGS | ||||
|           } | ||||
|           data-testid="settings-link" | ||||
|         > | ||||
|           <CustomIcon | ||||
|             name="settings" | ||||
|             className={`w-5 h-5 ${linkOverrideClassName}`} | ||||
|           /> | ||||
|           <span className="sr-only">Settings</span> | ||||
|           <Tooltip position="top" contentClassName="text-xs"> | ||||
|             Settings | ||||
|           </Tooltip> | ||||
|         </Link> | ||||
|         <NetworkMachineIndicator className={linkOverrideClassName} /> | ||||
|         {!location.pathname.startsWith(PATHS.HOME) && ( | ||||
|           <NetworkHealthIndicator /> | ||||
|         )} | ||||
|         <HelpMenu /> | ||||
|       </menu> | ||||
|     </section> | ||||
|   ) | ||||
|  | ||||
| @ -1,6 +1,39 @@ | ||||
| import { useEngineCommands } from './EngineCommands' | ||||
| import { Spinner } from './Spinner' | ||||
| import { CustomIcon } from './CustomIcon' | ||||
| import { StatusBarItemType } from './statusBar/statusBarTypes' | ||||
|  | ||||
| export const useModelStateStatus = (): StatusBarItemType => { | ||||
|   const [commands] = useEngineCommands() | ||||
|   const lastCommandType = commands[commands.length - 1]?.type | ||||
|  | ||||
|   let icon: StatusBarItemType['icon'] = 'loading' | ||||
|   const baseDataTestId = 'model-state-indicator' | ||||
|   let dataTestId = baseDataTestId | ||||
|  | ||||
|   if (lastCommandType === 'receive-reliable') { | ||||
|     icon = 'checkmark' | ||||
|     dataTestId = `${baseDataTestId}-receive-reliable` | ||||
|   } else if (lastCommandType === 'execution-done') { | ||||
|     icon = 'checkmark' | ||||
|     dataTestId = `${baseDataTestId}-execution-done` | ||||
|   } else if (lastCommandType === 'export-done') { | ||||
|     icon = 'checkmark' | ||||
|     dataTestId = `${baseDataTestId}-export-done` | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|     id: 'model-state-indicator', | ||||
|     label: '', | ||||
|     icon, | ||||
|     toolTip: { | ||||
|       children: 'Model state indicator', | ||||
|     }, | ||||
|     element: 'button', | ||||
|     onClick: () => {}, | ||||
|     'data-testid': dataTestId, | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const ModelStateIndicator = () => { | ||||
|   const [commands] = useEngineCommands() | ||||
|  | ||||
| @ -1085,7 +1085,10 @@ export const ModelingMachineProvider = ({ | ||||
|     > | ||||
|       {/* TODO #818: maybe pass reff down to children/app.ts or render app.tsx directly? | ||||
|       since realistically it won't ever have generic children that isn't app.tsx */} | ||||
|       <div className="h-screen overflow-hidden select-none" ref={streamRef}> | ||||
|       <div | ||||
|         className="flex flex-col h-screen overflow-hidden select-none" | ||||
|         ref={streamRef} | ||||
|       > | ||||
|         {children} | ||||
|       </div> | ||||
|     </ModelingMachineContext.Provider> | ||||
|  | ||||
| @ -6,6 +6,7 @@ import { useNetworkContext } from '../hooks/useNetworkContext' | ||||
| import { NetworkHealthState } from '../hooks/useNetworkStatus' | ||||
| import { toSync } from 'lib/utils' | ||||
| import { reportRejection } from 'lib/trap' | ||||
| import { StatusBarItemType } from './statusBar/statusBarTypes' | ||||
|  | ||||
| export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = { | ||||
|   [NetworkHealthState.Ok]: 'Connected', | ||||
| @ -64,14 +65,28 @@ const overallConnectionStateColor: Record<NetworkHealthState, IconColorConfig> = | ||||
|     }, | ||||
|   } | ||||
|  | ||||
| const overallConnectionStateIcon: Record< | ||||
|   NetworkHealthState, | ||||
|   ActionIconProps['icon'] | ||||
| > = { | ||||
| const overallConnectionStateIcon = { | ||||
|   [NetworkHealthState.Ok]: 'network', | ||||
|   [NetworkHealthState.Weak]: 'network', | ||||
|   [NetworkHealthState.Issue]: 'networkCrossedOut', | ||||
|   [NetworkHealthState.Disconnected]: 'networkCrossedOut', | ||||
| } as const | ||||
|  | ||||
| export const useNetworkHealthStatus = (): StatusBarItemType => { | ||||
|   const { overallState } = useNetworkContext() | ||||
|  | ||||
|   return { | ||||
|     id: 'network-health', | ||||
|     label: `Network health (${NETWORK_HEALTH_TEXT[overallState]})`, | ||||
|     hideLabel: true, | ||||
|     element: 'button', | ||||
|     className: overallConnectionStateColor[overallState].icon, | ||||
|     onClick: () => {}, | ||||
|     toolTip: { | ||||
|       children: 'View the health of your network connections', | ||||
|     }, | ||||
|     icon: overallConnectionStateIcon[overallState], | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const NetworkHealthIndicator = () => { | ||||
|  | ||||
| @ -13,7 +13,7 @@ import { isDesktop } from 'lib/isDesktop' | ||||
| import { ActionButton } from 'components/ActionButton' | ||||
| import { SettingsFieldInput } from './SettingsFieldInput' | ||||
| import toast from 'react-hot-toast' | ||||
| import { APP_VERSION } from 'routes/Settings' | ||||
| import { APP_VERSION } from 'lib/constants' | ||||
| import { PATHS } from 'lib/paths' | ||||
| import { createAndOpenNewProject, getSettingsFolderPaths } from 'lib/desktopFS' | ||||
| import { useDotDotSlash } from 'hooks/useDotDotSlash' | ||||
|  | ||||
							
								
								
									
										123
									
								
								src/components/StatusBar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								src/components/StatusBar.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,123 @@ | ||||
| import { useEffect } from 'react' | ||||
| import { ActionButton } from './ActionButton' | ||||
| import { StatusBarItemType } from './statusBar/statusBarTypes' | ||||
| import Tooltip, { TooltipProps } from './Tooltip' | ||||
| import { ActionIcon } from './ActionIcon' | ||||
|  | ||||
| export function StatusBar({ | ||||
|   globalItems, | ||||
|   localItems, | ||||
| }: { | ||||
|   globalItems: StatusBarItemType[] | ||||
|   localItems: StatusBarItemType[] | ||||
| }) { | ||||
|   useEffect(() => { | ||||
|     console.log('items', { | ||||
|       globalItems, | ||||
|       localItems, | ||||
|     }) | ||||
|   }, []) | ||||
|   return ( | ||||
|     <footer | ||||
|       id="statusbar" | ||||
|       className="relative z-10 flex justify-between items-center bg-chalkboard-20 dark:bg-chalkboard-90 text-chalkboard-80 dark:text-chalkboard-30 border-t border-t-chalkboard-30 dark:border-t-chalkboard-80" | ||||
|     > | ||||
|       <menu id="statusbar-globals" className="flex items-stretch"> | ||||
|         {globalItems.map((item, index) => ( | ||||
|           <StatusBarItem key={item.id} {...item} position={'left'} /> | ||||
|         ))} | ||||
|       </menu> | ||||
|       <menu id="statusbar-locals" className="flex items-stretch"> | ||||
|         {localItems.map((item, index) => ( | ||||
|           <StatusBarItem | ||||
|             key={item.id} | ||||
|             {...item} | ||||
|             position={index === localItems.length - 1 ? 'right' : 'middle'} | ||||
|           /> | ||||
|         ))} | ||||
|       </menu> | ||||
|     </footer> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| function StatusBarItem( | ||||
|   props: StatusBarItemType & { position: 'left' | 'middle' | 'right' } | ||||
| ) { | ||||
|   const defaultClassNames = `px-2 py-1 text-xs text-chalkboard-80 dark:text-chalkboard-30 rounded-none border-none hover:bg-chalkboard-30 dark:hover:bg-chalkboard-80 focus:bg-chalkboard-30 dark:focus:bg-chalkboard-80 hover:text-chalkboard-100 dark:hover:text-chalkboard-10 focustext-chalkboard-100 dark:focus:text-chalkboard-10  focus:outline-none focus-visible:ring-2 focus:ring-primary focus:ring-opacity-50` | ||||
|   const tooltipPosition: TooltipProps['position'] = | ||||
|     props.position === 'middle' ? 'top' : `top-${props.position}` | ||||
|  | ||||
|   switch (props.element) { | ||||
|     case 'button': | ||||
|       return ( | ||||
|         <ActionButton | ||||
|           Element="button" | ||||
|           iconStart={ | ||||
|             props.icon && { | ||||
|               icon: props.icon, | ||||
|               iconClassName: props.icon === 'loading' ? 'animate-spin' : '', | ||||
|               bgClassName: 'bg-transparent dark:bg-transparent', | ||||
|             } | ||||
|           } | ||||
|           className={defaultClassNames + ' ' + props.className} | ||||
|           data-testid={props['data-testid']} | ||||
|         > | ||||
|           {props.label && ( | ||||
|             <span className={props.hideLabel ? 'sr-only' : ''}> | ||||
|               {props.label} | ||||
|             </span> | ||||
|           )} | ||||
|           {props.toolTip && ( | ||||
|             <Tooltip {...props.toolTip} position={tooltipPosition} /> | ||||
|           )} | ||||
|         </ActionButton> | ||||
|       ) | ||||
|     case 'text': | ||||
|       return ( | ||||
|         <div | ||||
|           role="tooltip" | ||||
|           className={defaultClassNames + ' ' + props.className} | ||||
|         > | ||||
|           {props.icon && ( | ||||
|             <ActionIcon | ||||
|               icon={props.icon} | ||||
|               iconClassName={props.icon === 'loading' ? 'animate-spin' : ''} | ||||
|               bgClassName="bg-transparent dark:bg-transparent" | ||||
|             /> | ||||
|           )} | ||||
|           {props.label && ( | ||||
|             <span className={props.hideLabel ? 'sr-only' : ''}> | ||||
|               {props.label} | ||||
|             </span> | ||||
|           )} | ||||
|           {props.toolTip && ( | ||||
|             <Tooltip {...props.toolTip} position={tooltipPosition} /> | ||||
|           )} | ||||
|         </div> | ||||
|       ) | ||||
|     default: | ||||
|       return ( | ||||
|         <ActionButton | ||||
|           Element={props.element} | ||||
|           to={props.href} | ||||
|           iconStart={ | ||||
|             props.icon && { | ||||
|               icon: props.icon, | ||||
|               bgClassName: 'bg-transparent dark:bg-transparent', | ||||
|             } | ||||
|           } | ||||
|           className={defaultClassNames + ' ' + props.className} | ||||
|           data-testid={props['data-testid']} | ||||
|         > | ||||
|           {props.label && ( | ||||
|             <span className={props.hideLabel ? 'sr-only' : ''}> | ||||
|               {props.label} | ||||
|             </span> | ||||
|           )} | ||||
|           {props.toolTip && ( | ||||
|             <Tooltip {...props.toolTip} position={tooltipPosition} /> | ||||
|           )} | ||||
|         </ActionButton> | ||||
|       ) | ||||
|   } | ||||
| } | ||||
| @ -8,7 +8,7 @@ type LeftOrRight = 'left' | 'right' | ||||
| type Corner = `${TopOrBottom}-${LeftOrRight}` | ||||
| type TooltipPosition = TopOrBottom | LeftOrRight | Corner | ||||
|  | ||||
| interface TooltipProps extends React.PropsWithChildren { | ||||
| export interface TooltipProps extends React.PropsWithChildren { | ||||
|   position?: TooltipPosition | ||||
|   wrapperClassName?: string | ||||
|   contentClassName?: string | ||||
|  | ||||
							
								
								
									
										96
									
								
								src/components/statusBar/homeDefaultStatusBarItems.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								src/components/statusBar/homeDefaultStatusBarItems.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,96 @@ | ||||
| import openWindow from 'lib/openWindow' | ||||
| import { StatusBarItemType } from './statusBarTypes' | ||||
| import { reportRejection } from 'lib/trap' | ||||
| import { CoreDumpManager } from 'lib/coredump' | ||||
| import toast from 'react-hot-toast' | ||||
| import { coreDump } from 'lang/wasm' | ||||
| import { APP_VERSION } from 'lib/constants' | ||||
| import { Location } from 'react-router-dom' | ||||
| import { PATHS } from 'lib/paths' | ||||
|  | ||||
| export const homeDefaultStatusBarItems = ({ | ||||
|   coreDumpManager, | ||||
|   location, | ||||
| }: { | ||||
|   coreDumpManager?: CoreDumpManager | ||||
|   location: Location | ||||
| }): StatusBarItemType[] => [ | ||||
|   { | ||||
|     id: 'version', | ||||
|     element: 'externalLink', | ||||
|     label: `v${APP_VERSION}`, | ||||
|     href: `https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`, | ||||
|     toolTip: { | ||||
|       children: 'View the release notes on GitHub', | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     id: 'report-bug', | ||||
|     element: 'button', | ||||
|     label: 'Report a bug', | ||||
|     onClick: (event) => reportBug(event, { coreDumpManager }), | ||||
|     toolTip: { | ||||
|       children: 'Send your current app state to the developers for debugging', | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     id: 'settings', | ||||
|     element: 'link', | ||||
|     icon: 'settings', | ||||
|     href: | ||||
|       '.' + | ||||
|       PATHS.SETTINGS + | ||||
|       (location.pathname.includes(PATHS.FILE) ? '?tab=project' : ''), | ||||
|     'data-testid': 'settings-link', | ||||
|     label: 'Settings', | ||||
|     hideLabel: true, | ||||
|     toolTip: { | ||||
|       children: 'Settings', | ||||
|     }, | ||||
|   }, | ||||
| ] | ||||
|  | ||||
| function reportBug( | ||||
|   event: { | ||||
|     preventDefault: () => void | ||||
|     stopPropagation: () => void | ||||
|   }, | ||||
|   dependencies: { | ||||
|     coreDumpManager: CoreDumpManager | undefined | ||||
|   } | ||||
| ) { | ||||
|   event?.preventDefault() | ||||
|   event?.stopPropagation() | ||||
|   const { coreDumpManager } = dependencies | ||||
|  | ||||
|   if (!coreDumpManager) { | ||||
|     // open default reporting option | ||||
|     openWindow( | ||||
|       'https://github.com/KittyCAD/modeling-app/issues/new/choose' | ||||
|     ).catch(reportRejection) | ||||
|   } else { | ||||
|     toast | ||||
|       .promise( | ||||
|         coreDump(coreDumpManager, true), | ||||
|         { | ||||
|           loading: 'Preparing bug report...', | ||||
|           success: 'Bug report opened in new window', | ||||
|           error: 'Unable to export a core dump. Using default reporting.', | ||||
|         }, | ||||
|         { | ||||
|           success: { | ||||
|             // Note: this extended duration is especially important for Playwright e2e testing | ||||
|             // default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations | ||||
|             duration: 6000, | ||||
|           }, | ||||
|         } | ||||
|       ) | ||||
|       .catch((err: Error) => { | ||||
|         if (err) { | ||||
|           openWindow( | ||||
|             'https://github.com/KittyCAD/modeling-app/issues/new/choose' | ||||
|           ).catch(reportRejection) | ||||
|         } | ||||
|       }) | ||||
|   } | ||||
| } | ||||
							
								
								
									
										24
									
								
								src/components/statusBar/statusBarTypes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/components/statusBar/statusBarTypes.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | ||||
| import { CustomIconName } from 'components/CustomIcon' | ||||
| import { TooltipProps } from 'components/Tooltip' | ||||
|  | ||||
| export type StatusBarItemType = { | ||||
|   id: string | ||||
|   label: string | ||||
|   icon?: CustomIconName | ||||
|   hideLabel?: boolean | ||||
|   toolTip?: Omit<TooltipProps, 'position'> | ||||
|   className?: string | ||||
|   ['data-testid']?: string | ||||
| } & ( | ||||
|   | { | ||||
|       element: 'button' | ||||
|       onClick: (event: React.MouseEvent<HTMLButtonElement>) => void | ||||
|     } | ||||
|   | { | ||||
|       element: 'link' | 'externalLink' | ||||
|       href: string | ||||
|     } | ||||
|   | { | ||||
|       element: 'text' | ||||
|     } | ||||
| ) | ||||
| @ -1,4 +1,16 @@ | ||||
| import { NODE_ENV } from 'env' | ||||
| import { isTestEnv } from './isTestEnv' | ||||
| import { isDesktop } from './isDesktop' | ||||
|  | ||||
| export const APP_NAME = 'Modeling App' | ||||
| /** Version number of the app */ | ||||
| export const APP_VERSION = | ||||
|   isTestEnv && NODE_ENV === 'development' | ||||
|     ? '11.22.33' | ||||
|     : isDesktop() | ||||
|     ? // @ts-ignore | ||||
|       window.electron.packageJson.version | ||||
|     : 'main' | ||||
| /** Search string in new project names to increment as an index */ | ||||
| export const INDEX_IDENTIFIER = '$n' | ||||
| /** The maximum number of 0's to pad a default project name's index with */ | ||||
|  | ||||
| @ -2,7 +2,7 @@ import { CommandLog, EngineCommandManager } from 'lang/std/engineConnection' | ||||
| import { WebrtcStats } from 'wasm-lib/kcl/bindings/WebrtcStats' | ||||
| import { OsInfo } from 'wasm-lib/kcl/bindings/OsInfo' | ||||
| import { isDesktop } from 'lib/isDesktop' | ||||
| import { APP_VERSION } from 'routes/Settings' | ||||
| import { APP_VERSION } from 'lib/constants' | ||||
| import { UAParser } from 'ua-parser-js' | ||||
| import screenshot from 'lib/screenshot' | ||||
| import { VITE_KC_API_BASE_URL } from 'env' | ||||
|  | ||||
							
								
								
									
										4
									
								
								src/lib/isTestEnv.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/lib/isTestEnv.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| import { IS_PLAYWRIGHT_KEY } from '../../e2e/playwright/storageStates' | ||||
|  | ||||
| export const isTestEnv = | ||||
|   globalThis.window?.localStorage.getItem(IS_PLAYWRIGHT_KEY) === 'true' | ||||
							
								
								
									
										22
									
								
								src/lib/xStateValueToString.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/lib/xStateValueToString.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| import { AnyStateMachine, StateFrom } from 'xstate' | ||||
|  | ||||
| /** | ||||
|  * Convert an XState state value to a pretty string, | ||||
|  * with nested states separated by slashes | ||||
|  */ | ||||
| export function xStateValueToString( | ||||
|   stateValue: StateFrom<AnyStateMachine>['value'] | ||||
| ) { | ||||
|   const sep = ' / ' | ||||
|   let output = '' | ||||
|   let remainingValues = stateValue | ||||
|   let isFirstStep = true | ||||
|   while (remainingValues instanceof Object) { | ||||
|     const key: keyof typeof remainingValues = Object.keys(remainingValues)[0] | ||||
|     output += (isFirstStep ? '' : sep) + key | ||||
|     remainingValues = remainingValues[key] | ||||
|     isFirstStep = false | ||||
|   } | ||||
|   if (typeof remainingValues === 'string' && remainingValues.trim().length) | ||||
|     return output + sep + remainingValues.trim() | ||||
| } | ||||
| @ -12,19 +12,6 @@ import { SettingsSectionsList } from 'components/Settings/SettingsSectionsList' | ||||
| import { AllSettingsFields } from 'components/Settings/AllSettingsFields' | ||||
| import { AllKeybindingsFields } from 'components/Settings/AllKeybindingsFields' | ||||
| import { KeybindingsSectionsList } from 'components/Settings/KeybindingsSectionsList' | ||||
| import { isDesktop } from 'lib/isDesktop' | ||||
| import { IS_PLAYWRIGHT_KEY } from '../../e2e/playwright/storageStates' | ||||
| import { NODE_ENV } from 'env' | ||||
|  | ||||
| const isTestEnv = window?.localStorage.getItem(IS_PLAYWRIGHT_KEY) === 'true' | ||||
|  | ||||
| export const APP_VERSION = | ||||
|   isTestEnv && NODE_ENV === 'development' | ||||
|     ? '11.22.33' | ||||
|     : isDesktop() | ||||
|     ? // @ts-ignore | ||||
|       window.electron.packageJson.version | ||||
|     : 'main' | ||||
|  | ||||
| export const Settings = () => { | ||||
|   const navigate = useNavigate() | ||||
|  | ||||
| @ -4,12 +4,11 @@ import { VITE_KC_SITE_BASE_URL, VITE_KC_API_BASE_URL } from '../env' | ||||
| import { Themes, getSystemTheme } from '../lib/theme' | ||||
| import { PATHS } from 'lib/paths' | ||||
| import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' | ||||
| import { APP_NAME } from 'lib/constants' | ||||
| import { APP_NAME, APP_VERSION } from 'lib/constants' | ||||
| import { CSSProperties, useCallback, useState } from 'react' | ||||
| import { Logo } from 'components/Logo' | ||||
| import { CustomIcon } from 'components/CustomIcon' | ||||
| import { Link } from 'react-router-dom' | ||||
| import { APP_VERSION } from './Settings' | ||||
| import { openExternalBrowserIfDesktop } from 'lib/openWindow' | ||||
| import { toSync } from 'lib/utils' | ||||
| import { reportRejection } from 'lib/trap' | ||||
|  | ||||
		Reference in New Issue
	
	Block a user