diff --git a/frontend/bun.lock b/frontend/bun.lock index 6d30309..ce842f6 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -7,6 +7,7 @@ "dependencies": { "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-tooltip": "^1.2.8", "@sentry/react": "^10.30.0", "@t3-oss/env-core": "^0.13.8", "@tanstack/react-query": "^5.90.12", @@ -296,6 +297,8 @@ "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="], + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], @@ -310,6 +313,8 @@ "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], + "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.47", "", {}, "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw=="], @@ -982,6 +987,10 @@ "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], + "@radix-ui/react-tooltip/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], diff --git a/frontend/package.json b/frontend/package.json index 8585985..b9c6514 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ "dependencies": { "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-tooltip": "^1.2.8", "@sentry/react": "^10.30.0", "@t3-oss/env-core": "^0.13.8", "@tanstack/react-query": "^5.90.12", diff --git a/frontend/src/components/EditablePill.tsx b/frontend/src/components/EditablePill.tsx index e69b627..19e9da4 100644 --- a/frontend/src/components/EditablePill.tsx +++ b/frontend/src/components/EditablePill.tsx @@ -1,3 +1,4 @@ +import type {ReactNode} from 'react'; import {useCallback, useState} from 'react'; import {cva} from 'class-variance-authority'; import {cn} from 'utils/cn'; @@ -5,6 +6,7 @@ import {cn} from 'utils/cn'; import {Pill, type PillProps} from './Pill'; import {Popover, PopoverContent, PopoverTrigger} from './Popover'; import {Spinner} from './Spinner'; +import {Tooltip, TooltipContent, TooltipTrigger} from './Tooltip'; const optionRowStyles = cva([ 'w-full', @@ -39,6 +41,7 @@ export interface EditablePillProps { className?: string; getVariant?: (value: T) => PillProps['variant']; placeholder?: string; + disabledOptions?: Partial>; } export function EditablePill({ @@ -48,6 +51,7 @@ export function EditablePill({ className, getVariant, placeholder = 'Not set', + disabledOptions, }: EditablePillProps) { const [isOpen, setIsOpen] = useState(false); const [isSaving, setIsSaving] = useState(false); @@ -103,13 +107,16 @@ export function EditablePill({ case 'Enter': case ' ': event.preventDefault(); - if (focusedIndex >= 0) { + if ( + focusedIndex >= 0 && + !(disabledOptions && options[focusedIndex] in disabledOptions) + ) { handleSelect(options[focusedIndex]); } break; } }, - [isSaving, isOpen, focusedIndex, options, handleSelect] + [isSaving, isOpen, focusedIndex, options, handleSelect, disabledOptions] ); const variant = value @@ -140,23 +147,42 @@ export function EditablePill({ ? getVariant(option) : (option as PillProps['variant']); const isFocused = index === focusedIndex; - return ( + const isDisabled = disabledOptions !== undefined && option in disabledOptions; + const disabledMessage = disabledOptions?.[option]; + + const row = (
handleSelect(option)} + onClick={isDisabled ? undefined : () => handleSelect(option)} role="option" aria-selected={option === value} + aria-disabled={isDisabled} > {option}
); + + if (isDisabled) { + return ( + + {row} + + {disabledMessage} + + + ); + } + + return row; })} diff --git a/frontend/src/components/Tooltip.tsx b/frontend/src/components/Tooltip.tsx new file mode 100644 index 0000000..998e0dc --- /dev/null +++ b/frontend/src/components/Tooltip.tsx @@ -0,0 +1,64 @@ +import * as TooltipPrimitive from '@radix-ui/react-tooltip'; +import {cn} from 'utils/cn'; + +const TooltipProvider = TooltipPrimitive.Provider; + +const Tooltip = TooltipPrimitive.Root; + +const TooltipTrigger = TooltipPrimitive.Trigger; + +interface TooltipContentProps extends React.ComponentPropsWithoutRef< + typeof TooltipPrimitive.Content +> { + ref?: React.Ref; +} + +function TooltipContent({ + className, + sideOffset = 4, + ref, + children, + ...props +}: TooltipContentProps) { + return ( + + + {children} + + + + + + + + + ); +} + +export {TooltipProvider, Tooltip, TooltipTrigger, TooltipContent}; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 67a41b1..222cef9 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client'; import * as Sentry from '@sentry/react'; import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; import {createRouter, RouterProvider} from '@tanstack/react-router'; +import {TooltipProvider} from 'components/Tooltip'; import {routeTree} from './routeTree.gen'; @@ -51,7 +52,9 @@ if (!rootElement.innerHTML) { }); root.render( - + + + ); } diff --git a/frontend/src/routes/$incidentId/components/IncidentSummary.tsx b/frontend/src/routes/$incidentId/components/IncidentSummary.tsx index e029c4e..4429b09 100644 --- a/frontend/src/routes/$incidentId/components/IncidentSummary.tsx +++ b/frontend/src/routes/$incidentId/components/IncidentSummary.tsx @@ -106,6 +106,15 @@ export function IncidentSummary({incident}: IncidentSummaryProps) { value={incident.status} options={STATUS_OPTIONS} onSave={handleFieldChange('status')} + disabledOptions={{ + Postmortem: ( + <> + If you need a postmortem doc, use /inc fixed in the incident + channel. This is a temporary thing due to INC Bot owning the PM generation + and will be fixed in a future version of Firetower. + + ), + }} />