diff --git a/.changeset/eslint-react19-context-ref.md b/.changeset/eslint-react19-context-ref.md new file mode 100644 index 0000000000..05315e43a8 --- /dev/null +++ b/.changeset/eslint-react19-context-ref.md @@ -0,0 +1,10 @@ +--- +'@hyperdx/app': patch +--- + +Adopt React 19 context and ref APIs across the app and enforce them via ESLint. +Render `` directly instead of ``, use the `use` hook +instead of `useContext`, and pass `ref` as a regular prop instead of wrapping +components in `forwardRef`. The corresponding `@eslint-react/no-context-provider`, +`no-use-context`, and `no-forward-ref` rules are promoted to `error` and the +app's `--max-warnings` ceiling is lowered. Behavior is unchanged. diff --git a/packages/app/eslint.config.mjs b/packages/app/eslint.config.mjs index 9dced72ea3..c8cdd92ace 100644 --- a/packages/app/eslint.config.mjs +++ b/packages/app/eslint.config.mjs @@ -146,6 +146,12 @@ export default [ 'react-hook-form/no-use-watch': 'error', '@eslint-react/no-unstable-default-props': 'error', + // React 19 API adoption (all migrated): render instead of + // , use the `use` hook instead of `useContext`, and + // pass `ref` as a regular prop instead of wrapping in forwardRef. + '@eslint-react/no-context-provider': 'error', + '@eslint-react/no-use-context': 'error', + '@eslint-react/no-forward-ref': 'error', // useRef values must be named `ref` or end in `Ref` for readability. '@eslint-react/naming-convention/ref-name': 'error', '@typescript-eslint/ban-ts-comment': 'error', diff --git a/packages/app/package.json b/packages/app/package.json index 7c10867c1c..9e47025b8a 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -14,7 +14,7 @@ "build:clickhouse": "NEXT_PUBLIC_THEME=clickstack NEXT_PUBLIC_IS_LOCAL_MODE=true NEXT_PUBLIC_CLICKHOUSE_BUILD=true next build --webpack && node scripts/prepare-clickhouse-build-export.js", "run:clickhouse": "test -d out && npx rimraf tmp && mkdir tmp && cp -r out tmp/clickstack && echo 'visit http://localhost:3000/clickstack to start' && npx serve tmp -l 3000 || echo 'run build:clickhouse first'", "start": "next start", - "lint": "npx eslint . --ext .ts,.tsx --max-warnings 590", + "lint": "npx eslint . --ext .ts,.tsx --max-warnings 564", "lint:fix": "npx eslint . --ext .ts,.tsx --fix", "lint:styles": "stylelint **/*/*.{css,scss}", "ci:lint": "yarn lint && yarn tsc --noEmit && yarn lint:styles --quiet", diff --git a/packages/app/src/DBDashboardPage.tsx b/packages/app/src/DBDashboardPage.tsx index a2ef34836f..86922c2c9d 100644 --- a/packages/app/src/DBDashboardPage.tsx +++ b/packages/app/src/DBDashboardPage.tsx @@ -1,6 +1,5 @@ import { ForwardedRef, - forwardRef, useCallback, useEffect, useMemo, @@ -383,1162 +382,1150 @@ const whereLanguageParser = parseAsString.withDefault( typeof window !== 'undefined' ? (getStoredLanguage() ?? 'lucene') : 'lucene', ); -const Tile = forwardRef( - ( - { - chart, - dateRange, - onDuplicateClick, - onEditClick, - onDeleteClick, - onUpdateChart, - onMoveToGroup, - moveTargets, - granularity, - onTimeRangeSelect, - filters, - variables, - showAlertAnnotations, - showReleaseAnnotations, - isLive, - readOnly, +const Tile = ({ + chart, + dateRange, + onDuplicateClick, + onEditClick, + onDeleteClick, + onUpdateChart, + onMoveToGroup, + moveTargets, + granularity, + onTimeRangeSelect, + filters, + variables, + showAlertAnnotations, + showReleaseAnnotations, + isLive, + readOnly, - // Properties forwarded by grid layout - className, - style, - onMouseDown, - onMouseUp, - onTouchEnd, - children, - isHighlighted, - isSelected, - onSelect, - }: { - chart: Tile; - dateRange: [Date, Date]; - onDuplicateClick: () => void; - onEditClick: () => void; - onAddAlertClick?: () => void; - onDeleteClick: () => void; - onUpdateChart?: (chart: Tile) => void; - onMoveToGroup?: (containerId: string | undefined, tabId?: string) => void; - moveTargets?: MoveTarget[]; - onSettled?: () => void; - granularity: SQLInterval | undefined; - onTimeRangeSelect: (start: Date, end: Date) => void; - filters?: Filter[]; - variables?: ChartVariable[]; - // When true, draw alert firing/recovery annotations on this tile's chart. - showAlertAnnotations?: boolean; - // When true, draw release markers on this tile's chart. - showReleaseAnnotations?: boolean; - isLive?: boolean; - readOnly?: boolean; - - // Properties forwarded by grid layout - className?: string; - style?: React.CSSProperties; - onMouseDown?: (e: React.MouseEvent) => void; - onMouseUp?: (e: React.MouseEvent) => void; - onTouchEnd?: (e: React.TouchEvent) => void; - children?: React.ReactNode; // Resizer tooltip - isHighlighted?: boolean; - isSelected?: boolean; - onSelect?: (tileId: string) => void; + // Properties forwarded by grid layout + className, + style, + onMouseDown, + onMouseUp, + onTouchEnd, + children, + isHighlighted, + isSelected, + onSelect, + ref, +}: { + chart: Tile; + dateRange: [Date, Date]; + onDuplicateClick: () => void; + onEditClick: () => void; + onAddAlertClick?: () => void; + onDeleteClick: () => void; + onUpdateChart?: (chart: Tile) => void; + onMoveToGroup?: (containerId: string | undefined, tabId?: string) => void; + moveTargets?: MoveTarget[]; + onSettled?: () => void; + granularity: SQLInterval | undefined; + onTimeRangeSelect: (start: Date, end: Date) => void; + filters?: Filter[]; + variables?: ChartVariable[]; + // When true, draw alert firing/recovery annotations on this tile's chart. + showAlertAnnotations?: boolean; + // When true, draw release markers on this tile's chart. + showReleaseAnnotations?: boolean; + isLive?: boolean; + readOnly?: boolean; + + // Properties forwarded by grid layout + className?: string; + style?: React.CSSProperties; + onMouseDown?: (e: React.MouseEvent) => void; + onMouseUp?: (e: React.MouseEvent) => void; + onTouchEnd?: (e: React.TouchEvent) => void; + children?: React.ReactNode; // Resizer tooltip + isHighlighted?: boolean; + isSelected?: boolean; + onSelect?: (tileId: string) => void; + ref?: ForwardedRef; +}) => { + const [isFullscreen, setIsFullscreen] = useState(false); + const [isFocused, setIsFocused] = useState(false); + + // Lazy loading: only fetch a tile's data once it has scrolled into the + // browser viewport. React Grid Layout mounts every tile up front, so + // without this gating each tile would issue its ClickHouse query + // immediately, regardless of whether it is visible. We debounce the + // viewport signal (RGL briefly renders all tiles before the layout + // settles) and make visibility "sticky" so that a tile keeps its data + // once loaded instead of refetching every time it scrolls back into view. + const { ref: inViewportRef, inViewport } = useInViewport(); + const [debouncedInViewport] = useDebouncedValue(inViewport, 200); + // Latch to true the first time the tile becomes visible and never flip + // back, so a loaded tile keeps its data instead of refetching every time + // it scrolls out of and back into view. Adjusting state during render (the + // React-recommended pattern for deriving state from changing inputs) is + // cheaper than an effect and only fires once, since the condition is false + // after the first visible render. + const [hasBeenVisible, setHasBeenVisible] = useState(false); + if (debouncedInViewport && !hasBeenVisible) { + setHasBeenVisible(true); + } + + const { + userPreferences: { isUTC }, + } = useUserPreferences(); + + // Date range and granularity state local to the fullscreen view so that + // changing them does not propagate up to the dashboard. + const [fullscreenDateRange, setFullscreenDateRange] = + useState<[Date, Date]>(dateRange); + const [fullscreenInputValue, setFullscreenInputValue] = useState(() => + dateRangeToString(dateRange, isUTC), + ); + const [fullscreenGranularity, setFullscreenGranularity] = useState< + Granularity | 'auto' | undefined + >(() => (granularity as Granularity | undefined) ?? 'auto'); + + const openFullscreen = useCallback(() => { + // Reinitialize to the dashboard's current date range and granularity + // each time the fullscreen view is opened. + setFullscreenDateRange(dateRange); + setFullscreenInputValue(dateRangeToString(dateRange, isUTC)); + setFullscreenGranularity( + (granularity as Granularity | undefined) ?? 'auto', + ); + setIsFullscreen(true); + }, [dateRange, granularity, isUTC]); + + const handleFullscreenSearch = useCallback( + (value: string) => { + const [start, end] = parseTimeRangeInput(value, isUTC); + if (start != null && end != null) { + setFullscreenDateRange([start, end]); + } }, - ref: ForwardedRef, - ) => { - const [isFullscreen, setIsFullscreen] = useState(false); - const [isFocused, setIsFocused] = useState(false); - - // Lazy loading: only fetch a tile's data once it has scrolled into the - // browser viewport. React Grid Layout mounts every tile up front, so - // without this gating each tile would issue its ClickHouse query - // immediately, regardless of whether it is visible. We debounce the - // viewport signal (RGL briefly renders all tiles before the layout - // settles) and make visibility "sticky" so that a tile keeps its data - // once loaded instead of refetching every time it scrolls back into view. - const { ref: inViewportRef, inViewport } = useInViewport(); - const [debouncedInViewport] = useDebouncedValue(inViewport, 200); - // Latch to true the first time the tile becomes visible and never flip - // back, so a loaded tile keeps its data instead of refetching every time - // it scrolls out of and back into view. Adjusting state during render (the - // React-recommended pattern for deriving state from changing inputs) is - // cheaper than an effect and only fires once, since the condition is false - // after the first visible render. - const [hasBeenVisible, setHasBeenVisible] = useState(false); - if (debouncedInViewport && !hasBeenVisible) { - setHasBeenVisible(true); - } + [isUTC], + ); - const { - userPreferences: { isUTC }, - } = useUserPreferences(); + useEffect(() => { + if (isHighlighted) { + document + .getElementById(`chart-${chart.id}`) + ?.scrollIntoView({ behavior: 'smooth' }); + } + }, [chart.id, isHighlighted]); - // Date range and granularity state local to the fullscreen view so that - // changing them does not propagate up to the dashboard. - const [fullscreenDateRange, setFullscreenDateRange] = - useState<[Date, Date]>(dateRange); - const [fullscreenInputValue, setFullscreenInputValue] = useState( - () => dateRangeToString(dateRange, isUTC), - ); - const [fullscreenGranularity, setFullscreenGranularity] = useState< - Granularity | 'auto' | undefined - >(() => (granularity as Granularity | undefined) ?? 'auto'); - - const openFullscreen = useCallback(() => { - // Reinitialize to the dashboard's current date range and granularity - // each time the fullscreen view is opened. - setFullscreenDateRange(dateRange); - setFullscreenInputValue(dateRangeToString(dateRange, isUTC)); - setFullscreenGranularity( - (granularity as Granularity | undefined) ?? 'auto', - ); - setIsFullscreen(true); - }, [dateRange, granularity, isUTC]); - - const handleFullscreenSearch = useCallback( - (value: string) => { - const [start, end] = parseTimeRangeInput(value, isUTC); - if (start != null && end != null) { - setFullscreenDateRange([start, end]); + // YouTube-style 'f' key shortcut for fullscreen toggle + useHotkeys([ + [ + 'f', + () => { + if (!isFocused) return; + if (isFullscreen) { + setIsFullscreen(false); + } else { + openFullscreen(); } }, - [isUTC], - ); + ], + ]); - useEffect(() => { - if (isHighlighted) { - document - .getElementById(`chart-${chart.id}`) - ?.scrollIntoView({ behavior: 'smooth' }); - } - }, [chart.id, isHighlighted]); - - // YouTube-style 'f' key shortcut for fullscreen toggle - useHotkeys([ - [ - 'f', - () => { - if (!isFocused) return; - if (isFullscreen) { - setIsFullscreen(false); - } else { - openFullscreen(); - } - }, - ], - ]); + const [queriedConfig, setQueriedConfig] = useState< + ChartConfigWithDateRange | undefined + >(undefined); - const [queriedConfig, setQueriedConfig] = useState< - ChartConfigWithDateRange | undefined - >(undefined); + const { data: source, isFetched: isSourceFetched } = useSource({ + id: chart.config.source, + }); - const { data: source, isFetched: isSourceFetched } = useSource({ - id: chart.config.source, - }); + const isSourceMissing = + !!chart.config.source && isSourceFetched && source == null; + const isSourceUnset = + !!chart.config && + isBuilderSavedChartConfig(chart.config) && + displayTypeRequiresSource(chart.config.displayType) && + !chart.config.source; + + // `variables` is a new reference every time the dashboard's filter change. To ensure + // `tileVariables` is stable unless the tile's *referenced variables* actually change, + // we serialize the referenced subset and use changes in the serialized value to drive + // changes to `tileVariables`. + const serializedTileVariables = useMemo( + () => + !!variables && !isPromqlSavedChartConfig(chart.config) + ? JSON.stringify(filterReferencedVariables(chart.config, variables)) + : undefined, + [chart.config, variables], + ); + const tileVariables = useMemo( + () => + serializedTileVariables ? JSON.parse(serializedTileVariables) : undefined, + [serializedTileVariables], + ); - const isSourceMissing = - !!chart.config.source && isSourceFetched && source == null; - const isSourceUnset = - !!chart.config && - isBuilderSavedChartConfig(chart.config) && - displayTypeRequiresSource(chart.config.displayType) && - !chart.config.source; - - // `variables` is a new reference every time the dashboard's filter change. To ensure - // `tileVariables` is stable unless the tile's *referenced variables* actually change, - // we serialize the referenced subset and use changes in the serialized value to drive - // changes to `tileVariables`. - const serializedTileVariables = useMemo( - () => - !!variables && !isPromqlSavedChartConfig(chart.config) - ? JSON.stringify(filterReferencedVariables(chart.config, variables)) - : undefined, - [chart.config, variables], - ); - const tileVariables = useMemo( - () => - serializedTileVariables - ? JSON.parse(serializedTileVariables) - : undefined, - [serializedTileVariables], - ); + useEffect(() => { + if (isPromqlSavedChartConfig(chart.config)) { + if (source != null) { + setQueriedConfig({ + ...chart.config, + from: source.from, + connection: source.connection, + dateRange, + granularity, + }); + } + return; + } - useEffect(() => { - if (isPromqlSavedChartConfig(chart.config)) { - if (source != null) { - setQueriedConfig({ - ...chart.config, - from: source.from, - connection: source.connection, - dateRange, - granularity, - }); - } - return; + if (isRawSqlSavedChartConfig(chart.config)) { + // Some raw SQL charts don't have a source + if (!chart.config.source) { + setQueriedConfig({ + ...chart.config, + dateRange, + granularity, + filters, + variables: tileVariables, + }); + } else if (source != null) { + setQueriedConfig({ + ...chart.config, + // Populate these columns from the source to support Lucene-based filters and metric table macros + ...pick(source, [ + 'implicitColumnExpression', + 'useTextIndexForImplicitColumn', + 'from', + 'metricTables', + ]), + ...(isLogSource(source) + ? { bodyExpression: source.bodyExpression } + : {}), + sampleWeightExpression: getSampleWeightExpression(source), + dateRange, + granularity, + filters, + variables: tileVariables, + }); } - if (isRawSqlSavedChartConfig(chart.config)) { - // Some raw SQL charts don't have a source - if (!chart.config.source) { - setQueriedConfig({ - ...chart.config, - dateRange, - granularity, - filters, - variables: tileVariables, - }); - } else if (source != null) { - setQueriedConfig({ - ...chart.config, - // Populate these columns from the source to support Lucene-based filters and metric table macros - ...pick(source, [ - 'implicitColumnExpression', - 'useTextIndexForImplicitColumn', - 'from', - 'metricTables', - ]), - ...(isLogSource(source) - ? { bodyExpression: source.bodyExpression } - : {}), - sampleWeightExpression: getSampleWeightExpression(source), - dateRange, - granularity, - filters, - variables: tileVariables, - }); - } + return; + } - return; - } + if (source != null && isBuilderSavedChartConfig(chart.config)) { + const isMetricSource = source.kind === SourceKind.Metric; - if (source != null && isBuilderSavedChartConfig(chart.config)) { - const isMetricSource = source.kind === SourceKind.Metric; - - // TODO: will need to update this when we allow for multiple metrics per chart - const firstSelect = chart.config.select[0]; - const metricType = - isMetricSource && typeof firstSelect !== 'string' - ? firstSelect?.metricType - : undefined; - const tableName = getMetricTableName(source, metricType); - if (source.connection) { - setQueriedConfig({ - ...chart.config, - connection: source.connection, - dateRange, - granularity, - timestampValueExpression: source.timestampValueExpression, - from: { - databaseName: source.from?.databaseName || 'default', - tableName: tableName || '', - }, - implicitColumnExpression: - isLogSource(source) || isTraceSource(source) - ? source.implicitColumnExpression - : undefined, - useTextIndexForImplicitColumn: - isLogSource(source) || isTraceSource(source) - ? source.useTextIndexForImplicitColumn - : undefined, - bodyExpression: isLogSource(source) - ? source.bodyExpression + // TODO: will need to update this when we allow for multiple metrics per chart + const firstSelect = chart.config.select[0]; + const metricType = + isMetricSource && typeof firstSelect !== 'string' + ? firstSelect?.metricType + : undefined; + const tableName = getMetricTableName(source, metricType); + if (source.connection) { + setQueriedConfig({ + ...chart.config, + connection: source.connection, + dateRange, + granularity, + timestampValueExpression: source.timestampValueExpression, + from: { + databaseName: source.from?.databaseName || 'default', + tableName: tableName || '', + }, + implicitColumnExpression: + isLogSource(source) || isTraceSource(source) + ? source.implicitColumnExpression : undefined, - sampleWeightExpression: getSampleWeightExpression(source), - filters, - variables: tileVariables, - metricTables: isMetricSource ? source.metricTables : undefined, - }); - } + useTextIndexForImplicitColumn: + isLogSource(source) || isTraceSource(source) + ? source.useTextIndexForImplicitColumn + : undefined, + bodyExpression: isLogSource(source) + ? source.bodyExpression + : undefined, + sampleWeightExpression: getSampleWeightExpression(source), + filters, + variables: tileVariables, + metricTables: isMetricSource ? source.metricTables : undefined, + }); } - }, [source, chart, dateRange, granularity, filters, tileVariables]); + } + }, [source, chart, dateRange, granularity, filters, tileVariables]); - const [hovered, setHovered] = useState(false); + const [hovered, setHovered] = useState(false); - const alert = chart.config.alert; - const alertIndicatorColor = useMemo(() => { - if (!alert) { - return 'transparent'; - } - if (alert.state === AlertState.OK) { - return 'green'; - } - if (alert.silenced?.at) { - return 'yellow'; - } - if (alert.state === AlertState.PENDING) { - return 'orange'; - } - return 'red'; - }, [alert]); + const alert = chart.config.alert; + const alertIndicatorColor = useMemo(() => { + if (!alert) { + return 'transparent'; + } + if (alert.state === AlertState.OK) { + return 'green'; + } + if (alert.silenced?.at) { + return 'yellow'; + } + if (alert.state === AlertState.PENDING) { + return 'orange'; + } + return 'red'; + }, [alert]); - const alertTooltip = useMemo(() => { - if (!alert) { - return 'Add alert'; - } - let tooltip = `Has alert and is in ${alert.state} state`; - if (alert.silenced?.at) { - const silencedAt = new Date(alert.silenced.at); - // eslint-disable-next-line no-restricted-syntax - tooltip += `. Ack'd ${formatRelative(silencedAt, new Date())}`; - } - return tooltip; - }, [alert]); - - // Only DBTimeChart draws annotations (see the render below) — a tile - // showing a table, number, pie, ... discards them. Both annotation queries - // stay idle for those rather than paying for a result nothing can show. - const tileCanDrawAnnotations = isTimeSeriesDisplayType( - chart.config.displayType, - ); + const alertTooltip = useMemo(() => { + if (!alert) { + return 'Add alert'; + } + let tooltip = `Has alert and is in ${alert.state} state`; + if (alert.silenced?.at) { + const silencedAt = new Date(alert.silenced.at); + // eslint-disable-next-line no-restricted-syntax + tooltip += `. Ack'd ${formatRelative(silencedAt, new Date())}`; + } + return tooltip; + }, [alert]); - // Firing/recovery markers for this tile's alert, scoped to the *visible* - // window — the fullscreen range while the fullscreen view is open, else the - // dashboard range (off unless the dashboard toggle is on). - const alertAnnotations = useAlertAnnotations( - alert?.id, - isFullscreen ? fullscreenDateRange : dateRange, - showAlertAnnotations && tileCanDrawAnnotations, - ); + const tileCanDrawAnnotations = isTimeSeriesDisplayType( + chart.config.displayType, + ); - // Release markers, over the same visible window. Scoped to this tile: the - // query runs against the tile's own source with the tile's own predicates, - // so a chart filtered to one service isn't annotated with another's - // releases. Tiles sharing a source and filters share one query. - // - // A time chart's filter lives in each series' `aggCondition`, not in the - // statement-level `where` (the editor clears that one), so `select` has to - // come along for the scoping to mean anything — `where` is carried for the - // configs that do set it, e.g. an imported dashboard. - const builderConfig = isBuilderSavedChartConfig(chart.config) - ? chart.config - : undefined; - const releaseAnnotations = useReleaseAnnotations( - isFullscreen ? fullscreenDateRange : dateRange, - showReleaseAnnotations && tileCanDrawAnnotations, - { - source, - where: builderConfig?.where, - whereLanguage: builderConfig?.whereLanguage, - select: builderConfig?.select, - filters, - }, - ); + // Firing/recovery markers for this tile's alert, scoped to the *visible* + // window — the fullscreen range while the fullscreen view is open, else the + // dashboard range (off unless the dashboard toggle is on). + const alertAnnotations = useAlertAnnotations( + alert?.id, + isFullscreen ? fullscreenDateRange : dateRange, + showAlertAnnotations && tileCanDrawAnnotations, + ); - const annotations = useMemo( - () => mergeAnnotations(alertAnnotations, releaseAnnotations), - [alertAnnotations, releaseAnnotations], - ); + // Release markers, over the same visible window. Scoped to this tile: the + // query runs against the tile's own source with the tile's own predicates, + // so a chart filtered to one service isn't annotated with another's + // releases. Tiles sharing a source and filters share one query. + // + // A time chart's filter lives in each series' `aggCondition`, not in the + // statement-level `where` (the editor clears that one), so `select` has to + // come along for the scoping to mean anything — `where` is carried for the + // configs that do set it, e.g. an imported dashboard. + const builderConfig = isBuilderSavedChartConfig(chart.config) + ? chart.config + : undefined; + const releaseAnnotations = useReleaseAnnotations( + isFullscreen ? fullscreenDateRange : dateRange, + showReleaseAnnotations && tileCanDrawAnnotations, + { + source, + where: builderConfig?.where, + whereLanguage: builderConfig?.whereLanguage, + select: builderConfig?.select, + filters, + }, + ); - const filterWarning = useMemo(() => { - const doFiltersExist = !!filters?.filter( - f => (f.type === 'lucene' || f.type === 'sql') && f.condition.trim(), - )?.length; - const doLuceneFiltersExist = !!filters?.filter( - f => f.type === 'lucene' && f.condition.trim(), - )?.length; - - if ( - !doFiltersExist || - !queriedConfig || - !isRawSqlChartConfig(queriedConfig) - ) - return null; - - const isMissingSourceForFiltering = !queriedConfig.source; - const missingFiltersMacro = isMissingFiltersMacro( - queriedConfig.sqlTemplate, - ); - const isMetricsSourceWithLuceneFilter = - source?.kind === SourceKind.Metric && doLuceneFiltersExist; - - if ( - !isMissingSourceForFiltering && - !missingFiltersMacro && - !isMetricsSourceWithLuceneFilter - ) - return null; - - const message = missingFiltersMacro - ? 'Filters may not be applied correctly because the SQL does not include the recommended $__filters macro' - : isMetricsSourceWithLuceneFilter - ? 'Lucene filters are not applied because they are not supported for metrics sources.' - : 'Filters are not applied because no Source is set for this chart'; + const annotations = useMemo( + () => mergeAnnotations(alertAnnotations, releaseAnnotations), + [alertAnnotations, releaseAnnotations], + ); - return ( - - - - ); - }, [filters, queriedConfig, source]); + const filterWarning = useMemo(() => { + const doFiltersExist = !!filters?.filter( + f => (f.type === 'lucene' || f.type === 'sql') && f.condition.trim(), + )?.length; + const doLuceneFiltersExist = !!filters?.filter( + f => f.type === 'lucene' && f.condition.trim(), + )?.length; - const replaySearchUrl = useMemo(() => { - return buildDashboardReplaySearchUrl({ - source, - config: queriedConfig, - dateRange, - }); - }, [dateRange, queriedConfig, source]); - - const hoverToolbar = useMemo(() => { - if (readOnly) return null; - - const isRawSql = isRawSqlSavedChartConfig(chart.config); - const isPromQL = isPromqlSavedChartConfig(chart.config); - const displayTypeSupportsAlerts = isRawSql - ? displayTypeSupportsRawSqlAlerts(chart.config.displayType) - : isPromQL - ? displayTypeSupportsPromQLAlerts(chart.config.displayType) - : displayTypeSupportsBuilderAlerts(chart.config.displayType); - const canMoveToGroup = - onMoveToGroup && moveTargets && moveTargets.length > 0; - return ( - e.stopPropagation()} - key="hover-toolbar" - my={2} // Margin to ensure that the Alert Indicator doesn't clip on non-Line/Bar display types - > - {replaySearchUrl && ( - - - - - - )} + if ( + !doFiltersExist || + !queriedConfig || + !isRawSqlChartConfig(queriedConfig) + ) + return null; + + const isMissingSourceForFiltering = !queriedConfig.source; + const missingFiltersMacro = isMissingFiltersMacro( + queriedConfig.sqlTemplate, + ); + const isMetricsSourceWithLuceneFilter = + source?.kind === SourceKind.Metric && doLuceneFiltersExist; - {displayTypeSupportsAlerts && - (alert ? ( - // Existing alert: bell with a colored status dot indicator. - - - - - - - - ) : ( - // No alert yet: a dedicated "bell +" icon reads clearly on any - // background, unlike an overlaid indicator badge. - - - - - - ))} + if ( + !isMissingSourceForFiltering && + !missingFiltersMacro && + !isMetricsSourceWithLuceneFilter + ) + return null; + + const message = missingFiltersMacro + ? 'Filters may not be applied correctly because the SQL does not include the recommended $__filters macro' + : isMetricsSourceWithLuceneFilter + ? 'Lucene filters are not applied because they are not supported for metrics sources.' + : 'Filters are not applied because no Source is set for this chart'; - {/* Fullscreen is a primary action, so it lives directly in the - toolbar rather than buried in the "More actions" menu. */} - + return ( + + + + ); + }, [filters, queriedConfig, source]); + + const replaySearchUrl = useMemo(() => { + return buildDashboardReplaySearchUrl({ + source, + config: queriedConfig, + dateRange, + }); + }, [dateRange, queriedConfig, source]); + + const hoverToolbar = useMemo(() => { + if (readOnly) return null; + + const isRawSql = isRawSqlSavedChartConfig(chart.config); + const isPromQL = isPromqlSavedChartConfig(chart.config); + const displayTypeSupportsAlerts = isRawSql + ? displayTypeSupportsRawSqlAlerts(chart.config.displayType) + : isPromQL + ? displayTypeSupportsPromQLAlerts(chart.config.displayType) + : displayTypeSupportsBuilderAlerts(chart.config.displayType); + const canMoveToGroup = + onMoveToGroup && moveTargets && moveTargets.length > 0; + return ( + e.stopPropagation()} + key="hover-toolbar" + my={2} // Margin to ensure that the Alert Indicator doesn't clip on non-Line/Bar display types + > + {replaySearchUrl && ( + openFullscreen()} mr={4} > - + + )} - - - + {displayTypeSupportsAlerts && + (alert ? ( + // Existing alert: bell with a colored status dot indicator. + + - + - - e.stopPropagation()}> - } - onClick={onDuplicateClick} - > - Duplicate - - } + + ) : ( + // No alert yet: a dedicated "bell +" icon reads clearly on any + // background, unlike an overlaid indicator badge. + + - Edit - - {canMoveToGroup && ( - <> - - Move to Group - {chart.containerId && ( + + + + ))} + + {/* Fullscreen is a primary action, so it lives directly in the + toolbar rather than buried in the "More actions" menu. */} + + openFullscreen()} + mr={4} + > + + + + + + + + + + + + + e.stopPropagation()}> + } + onClick={onDuplicateClick} + > + Duplicate + + } + onClick={onEditClick} + > + Edit + + {canMoveToGroup && ( + <> + + Move to Group + {chart.containerId && ( + } + onClick={() => onMoveToGroup(undefined)} + > + (Ungrouped) + + )} + {moveTargets + .filter( + t => + !( + t.containerId === chart.containerId && + t.tabId === chart.tabId + ), + ) + .map(t => ( } - onClick={() => onMoveToGroup(undefined)} + onClick={() => onMoveToGroup(t.containerId, t.tabId)} > - (Ungrouped) - - )} - {moveTargets - .filter( - t => - !( - t.containerId === chart.containerId && - t.tabId === chart.tabId - ), - ) - .map(t => ( - } - onClick={() => onMoveToGroup(t.containerId, t.tabId)} - > - {t.allTabs ? ( - - {t.allTabs.map((tab, i) => ( - - {i > 0 && ( - - {' | '} - - )} + {t.allTabs ? ( + + {t.allTabs.map((tab, i) => ( + + {i > 0 && ( - {tab.title} + {' | '} + )} + + {tab.title} - ))} - - ) : ( - t.label - )} - - ))} - - )} - - } - onClick={onDeleteClick} - > - Delete - - - - - ); - }, [ - alert, - alertIndicatorColor, - alertTooltip, - moveTargets, - replaySearchUrl, - chart.config, - chart.id, - chart.containerId, - chart.tabId, - onDeleteClick, - onDuplicateClick, - onEditClick, - onMoveToGroup, - openFullscreen, - readOnly, - ]); - - // Flat Menu.Item list for the collapsed (narrow-tile) toolbar. - // Merges the alert action + all kebab items into a single flat list - // so ChartContainer can render them without nested menus. - const collapsedMenuItems = useMemo(() => { - if (readOnly) return null; - - const isRawSql = isRawSqlSavedChartConfig(chart.config); - const isPromQL = isPromqlSavedChartConfig(chart.config); - const showAlerts = isRawSql - ? displayTypeSupportsRawSqlAlerts(chart.config.displayType) - : isPromQL - ? displayTypeSupportsPromQLAlerts(chart.config.displayType) - : displayTypeSupportsBuilderAlerts(chart.config.displayType); - const canMoveToGroup = - onMoveToGroup && moveTargets && moveTargets.length > 0; - return ( - <> - {replaySearchUrl && ( + + ))} + + ) : ( + t.label + )} + + ))} + + )} + } + data-testid={`tile-delete-button-${chart.id}`} + color="red" + leftSection={} + onClick={onDeleteClick} > - Replay search + Delete - )} - {showAlerts && ( - <> - : - } - onClick={onEditClick} - > - {alertTooltip} - - - - )} - } - onClick={onDuplicateClick} - > - Duplicate - - } - onClick={() => openFullscreen()} - > - View fullscreen - + + + + ); + }, [ + alert, + alertIndicatorColor, + alertTooltip, + moveTargets, + replaySearchUrl, + chart.config, + chart.id, + chart.containerId, + chart.tabId, + onDeleteClick, + onDuplicateClick, + onEditClick, + onMoveToGroup, + openFullscreen, + readOnly, + ]); + + // Flat Menu.Item list for the collapsed (narrow-tile) toolbar. + // Merges the alert action + all kebab items into a single flat list + // so ChartContainer can render them without nested menus. + const collapsedMenuItems = useMemo(() => { + if (readOnly) return null; + + const isRawSql = isRawSqlSavedChartConfig(chart.config); + const isPromQL = isPromqlSavedChartConfig(chart.config); + const showAlerts = isRawSql + ? displayTypeSupportsRawSqlAlerts(chart.config.displayType) + : isPromQL + ? displayTypeSupportsPromQLAlerts(chart.config.displayType) + : displayTypeSupportsBuilderAlerts(chart.config.displayType); + const canMoveToGroup = + onMoveToGroup && moveTargets && moveTargets.length > 0; + return ( + <> + {replaySearchUrl && ( } - onClick={onEditClick} + component={Link} + href={replaySearchUrl} + target="_blank" + rel="noopener noreferrer" + leftSection={} > - Edit + Replay search - {canMoveToGroup && ( - <> - - Move to Group - {chart.containerId && ( + )} + {showAlerts && ( + <> + : + } + onClick={onEditClick} + > + {alertTooltip} + + + + )} + } + onClick={onDuplicateClick} + > + Duplicate + + } + onClick={() => openFullscreen()} + > + View fullscreen + + } onClick={onEditClick}> + Edit + + {canMoveToGroup && ( + <> + + Move to Group + {chart.containerId && ( + } + onClick={() => onMoveToGroup(undefined)} + > + (Ungrouped) + + )} + {moveTargets + .filter( + t => + !( + t.containerId === chart.containerId && + t.tabId === chart.tabId + ), + ) + .map(t => ( } - onClick={() => onMoveToGroup(undefined)} + onClick={() => onMoveToGroup(t.containerId, t.tabId)} > - (Ungrouped) - - )} - {moveTargets - .filter( - t => - !( - t.containerId === chart.containerId && - t.tabId === chart.tabId - ), - ) - .map(t => ( - } - onClick={() => onMoveToGroup(t.containerId, t.tabId)} - > - {t.allTabs ? ( - - {t.allTabs.map((tab, i) => ( - - {i > 0 && ( - - {' | '} - - )} + {t.allTabs ? ( + + {t.allTabs.map((tab, i) => ( + + {i > 0 && ( - {tab.title} + {' | '} + )} + + {tab.title} - ))} - - ) : ( - t.label - )} - - ))} - - )} - - } - onClick={onDeleteClick} - > - Delete - - - ); - }, [ - alert, - alertTooltip, - moveTargets, - replaySearchUrl, - chart.config, - chart.containerId, - chart.tabId, - onDeleteClick, - onDuplicateClick, - onEditClick, - onMoveToGroup, - openFullscreen, - readOnly, - ]); - - const title = useMemo( - () => - chart.config.name ? ( - {chart.config.name} - ) : undefined, - [chart.config.name], + + ))} + + ) : ( + t.label + )} + + ))} + + )} + + } + onClick={onDeleteClick} + > + Delete + + ); + }, [ + alert, + alertTooltip, + moveTargets, + replaySearchUrl, + chart.config, + chart.containerId, + chart.tabId, + onDeleteClick, + onDuplicateClick, + onEditClick, + onMoveToGroup, + openFullscreen, + readOnly, + ]); - // Render chart content (used in both tile and fullscreen views) - const renderChartContent = useCallback( - (hideToolbar: boolean = false, isFullscreenView: boolean = false) => { - // Tile-level actions (alert bell + kebab) render as a suffix so they - // sit to the right of each chart's own controls (display switcher, - // granularity, etc.), keeping the kebab at the far right edge. - const toolbarPrefixItems = [filterWarning]; - const toolbarSuffixItems = hideToolbar ? [] : [hoverToolbar]; - // Combined + ordered for containers that only accept `toolbarItems` - // (they have no chart-specific controls to interleave). - const toolbar = [...toolbarPrefixItems, ...toolbarSuffixItems]; - const keyPrefix = isFullscreenView ? 'fullscreen' : 'tile'; - - // The fullscreen view is always visible, so it should always load. - // In the tile (grid) view, gate data fetching on viewport visibility. - const chartEnabled = isFullscreenView ? true : hasBeenVisible; - - // Use the fullscreen-local date range and granularity when rendering - // inside the fullscreen modal so that changing them does not affect - // the dashboard. - const effectiveDateRange = isFullscreenView - ? fullscreenDateRange - : dateRange; - const effectiveGranularity = isFullscreenView - ? fullscreenGranularity - : queriedConfig?.granularity; - const effectiveQueriedConfig = queriedConfig - ? { - ...queriedConfig, - dateRange: effectiveDateRange, - granularity: effectiveGranularity, - } - : undefined; + const title = useMemo( + () => + chart.config.name ? ( + {chart.config.name} + ) : undefined, + [chart.config.name], + ); - // Markdown charts may not have queriedConfig, if config.source is not set - const effectiveMarkdownConfig = effectiveQueriedConfig ?? chart.config; + // Render chart content (used in both tile and fullscreen views) + const renderChartContent = useCallback( + (hideToolbar: boolean = false, isFullscreenView: boolean = false) => { + // Tile-level actions (alert bell + kebab) render as a suffix so they + // sit to the right of each chart's own controls (display switcher, + // granularity, etc.), keeping the kebab at the far right edge. + const toolbarPrefixItems = [filterWarning]; + const toolbarSuffixItems = hideToolbar ? [] : [hoverToolbar]; + // Combined + ordered for containers that only accept `toolbarItems` + // (they have no chart-specific controls to interleave). + const toolbar = [...toolbarPrefixItems, ...toolbarSuffixItems]; + const keyPrefix = isFullscreenView ? 'fullscreen' : 'tile'; + + // The fullscreen view is always visible, so it should always load. + // In the tile (grid) view, gate data fetching on viewport visibility. + const chartEnabled = isFullscreenView ? true : hasBeenVisible; + + // Use the fullscreen-local date range and granularity when rendering + // inside the fullscreen modal so that changing them does not affect + // the dashboard. + const effectiveDateRange = isFullscreenView + ? fullscreenDateRange + : dateRange; + const effectiveGranularity = isFullscreenView + ? fullscreenGranularity + : queriedConfig?.granularity; + const effectiveQueriedConfig = queriedConfig + ? { + ...queriedConfig, + dateRange: effectiveDateRange, + granularity: effectiveGranularity, + } + : undefined; - return ( - - An error occurred while rendering the chart. - - } - > - {isSourceMissing ? ( - - - - The data source for this tile no longer exists. Edit the - tile to select a new source. - - - - ) : isSourceUnset ? ( - - - - The data source for this tile is not set. Edit the tile to - select a data source. - - - - ) : ( - <> - {(effectiveQueriedConfig?.displayType === DisplayType.Line || - effectiveQueriedConfig?.displayType === - DisplayType.StackedBar) && ( - + An error occurred while rendering the chart. + + } + > + {isSourceMissing ? ( + + + + The data source for this tile no longer exists. Edit the tile + to select a new source. + + + + ) : isSourceUnset ? ( + + + + The data source for this tile is not set. Edit the tile to + select a data source. + + + + ) : ( + <> + {(effectiveQueriedConfig?.displayType === DisplayType.Line || + effectiveQueriedConfig?.displayType === + DisplayType.StackedBar) && ( + setFullscreenDateRange([start, end]) + : onTimeRangeSelect + } + setDisplayType={ + readOnly + ? undefined + : displayType => { + onUpdateChart?.({ + ...chart, + config: { + ...chart.config, + displayType, + }, + }); + } + } + /> + )} + {effectiveQueriedConfig?.displayType === DisplayType.Table && ( + + setFullscreenDateRange([start, end]) - : onTimeRangeSelect - } - setDisplayType={ - readOnly - ? undefined - : displayType => { - onUpdateChart?.({ - ...chart, - config: { - ...chart.config, - displayType, - }, - }); - } + variant="default" + getRowSearchLink={ + isBuilderChartConfig(effectiveQueriedConfig) + ? row => + buildTableRowSearchUrl({ + row, + source, + config: effectiveQueriedConfig, + dateRange: effectiveDateRange, + }) + : undefined } /> - )} - {effectiveQueriedConfig?.displayType === DisplayType.Table && ( - - - buildTableRowSearchUrl({ - row, - source, - config: effectiveQueriedConfig, - dateRange: effectiveDateRange, - }) - : undefined - } - /> - - )} - {effectiveQueriedConfig?.displayType === DisplayType.Number && ( - + )} + {effectiveQueriedConfig?.displayType === DisplayType.Number && ( + + )} + {effectiveQueriedConfig?.displayType === DisplayType.Pie && ( + + )} + {effectiveQueriedConfig?.displayType === DisplayType.Bar && ( + + )} + {effectiveQueriedConfig?.displayType === DisplayType.Heatmap && + isBuilderChartConfig(effectiveQueriedConfig) && ( + )} - {effectiveQueriedConfig?.displayType === DisplayType.Pie && ( - )} - {effectiveQueriedConfig?.displayType === DisplayType.Bar && ( - - )} - {effectiveQueriedConfig?.displayType === DisplayType.Heatmap && - isBuilderChartConfig(effectiveQueriedConfig) && ( - + - )} - {effectiveMarkdownConfig?.displayType === - DisplayType.Markdown && - 'markdown' in effectiveMarkdownConfig && ( - + )} + {effectiveQueriedConfig?.displayType === + DisplayType.EventPatterns && + isBuilderChartConfig(effectiveQueriedConfig) && + isBuilderSavedChartConfig(chart.config) && ( + + 0 && + isSingleExpression(effectiveQueriedConfig.select) + ? effectiveQueriedConfig.select + : undefined) ?? + (source ? (getEventBody(source) ?? '') : '') + } + totalCountConfig={{ + ...effectiveQueriedConfig, + displayType: DisplayType.Table, + dateRange: effectiveDateRange, + select: 'count() as total', + groupBy: undefined, + orderBy: undefined, + granularity: undefined, + }} + totalCountQueryKeyPrefix={`dashboard-patterns-${chart.id}`} /> - )} - {effectiveQueriedConfig?.displayType === DisplayType.Search && - isBuilderChartConfig(effectiveQueriedConfig) && - isBuilderSavedChartConfig(chart.config) && ( - - - - )} - {effectiveQueriedConfig?.displayType === - DisplayType.EventPatterns && - isBuilderChartConfig(effectiveQueriedConfig) && - isBuilderSavedChartConfig(chart.config) && ( - - 0 && - isSingleExpression(effectiveQueriedConfig.select) - ? effectiveQueriedConfig.select - : undefined) ?? - (source ? (getEventBody(source) ?? '') : '') - } - totalCountConfig={{ - ...effectiveQueriedConfig, - displayType: DisplayType.Table, - dateRange: effectiveDateRange, - select: 'count() as total', - groupBy: undefined, - orderBy: undefined, - granularity: undefined, - }} - totalCountQueryKeyPrefix={`dashboard-patterns-${chart.id}`} - /> - - )} - - )} - - ); - }, - [ - hoverToolbar, - queriedConfig, - title, - chart, - onTimeRangeSelect, - onUpdateChart, - source, - dateRange, - fullscreenDateRange, - fullscreenGranularity, - filterWarning, - isSourceMissing, - isSourceUnset, - hasBeenVisible, - annotations, - isLive, - readOnly, - ], - ); + + )} + + )} + + ); + }, + [ + hoverToolbar, + queriedConfig, + title, + chart, + onTimeRangeSelect, + onUpdateChart, + source, + dateRange, + fullscreenDateRange, + fullscreenGranularity, + filterWarning, + isSourceMissing, + isSourceUnset, + hasBeenVisible, + annotations, + isLive, + readOnly, + ], + ); - return ( - <> + return ( + <> +
{ + setHovered(true); + setIsFocused(true); + }} + onMouseLeave={() => { + setHovered(false); + setIsFocused(false); + }} + key={chart.id} + ref={ref} + style={{ + ...style, + ...(isSelected + ? { + outline: '2px solid var(--color-outline-focus)', + outlineOffset: -2, + } + : {}), + }} + onClick={e => { + if (e.shiftKey && onSelect) { + e.preventDefault(); + onSelect(chart.id); + } + }} + onMouseDown={onMouseDown} + onMouseUp={onMouseUp} + onTouchEnd={onTouchEnd} + > + {hovered && !readOnly && ( +
+ )}
{ - setHovered(true); - setIsFocused(true); - }} - onMouseLeave={() => { - setHovered(false); - setIsFocused(false); - }} - key={chart.id} - ref={ref} - style={{ - ...style, - ...(isSelected - ? { - outline: '2px solid var(--color-outline-focus)', - outlineOffset: -2, - } - : {}), - }} - onClick={e => { - if (e.shiftKey && onSelect) { - e.preventDefault(); - onSelect(chart.id); - } - }} - onMouseDown={onMouseDown} - onMouseUp={onMouseUp} - onTouchEnd={onTouchEnd} + ref={inViewportRef} + className="fs-7 text-muted flex-grow-1 overflow-hidden cursor-default" + style={{ paddingInline: DASHBOARD_TILE_PADDING_INLINE }} + onMouseDown={e => e.stopPropagation()} > - {hovered && !readOnly && ( -
- )} -
e.stopPropagation()} + - - - {renderChartContent(readOnly)} - - -
- {children} + + {renderChartContent(readOnly)} + +
+ {children} +
- {/* Fullscreen Modal */} - setIsFullscreen(false)} - > - {isFullscreen && ( - - - - - - - {renderChartContent(true, true)} - + {/* Fullscreen Modal */} + setIsFullscreen(false)} + > + {isFullscreen && ( + + + + - )} - - - ); - }, -); + + {renderChartContent(true, true)} + + + )} + + + ); +}; const EditTileModal = ({ dashboardId, @@ -1603,7 +1590,7 @@ const EditTileModal = ({ zIndex={modalZIndex} > {chart != null && ( - + {/* Isolate chart cross-syncing to this edit modal: the preview chart must not drive shadow tooltips on the dashboard tiles behind it. */} @@ -1630,7 +1617,7 @@ const EditTileModal = ({ /> - + )} ); diff --git a/packages/app/src/NamespaceDetailsSidePanel.tsx b/packages/app/src/NamespaceDetailsSidePanel.tsx index b53dd918fd..1027c55549 100644 --- a/packages/app/src/NamespaceDetailsSidePanel.tsx +++ b/packages/app/src/NamespaceDetailsSidePanel.tsx @@ -337,7 +337,7 @@ export default function NamespaceDetailsSidePanel({ }, }} > - +
-
+ ); } diff --git a/packages/app/src/NodeDetailsSidePanel.tsx b/packages/app/src/NodeDetailsSidePanel.tsx index 08a50ffa61..90ca592317 100644 --- a/packages/app/src/NodeDetailsSidePanel.tsx +++ b/packages/app/src/NodeDetailsSidePanel.tsx @@ -350,7 +350,7 @@ export default function NodeDetailsSidePanel({ }, }} > - +
-
+ ); } diff --git a/packages/app/src/PodDetailsSidePanel.tsx b/packages/app/src/PodDetailsSidePanel.tsx index 101efac290..7ee6bc5451 100644 --- a/packages/app/src/PodDetailsSidePanel.tsx +++ b/packages/app/src/PodDetailsSidePanel.tsx @@ -345,7 +345,7 @@ export default function PodDetailsSidePanel({ }, }} > - +
- + ); } diff --git a/packages/app/src/SessionEventList.tsx b/packages/app/src/SessionEventList.tsx index 4249189650..90089def9c 100644 --- a/packages/app/src/SessionEventList.tsx +++ b/packages/app/src/SessionEventList.tsx @@ -42,56 +42,52 @@ const EVENT_ROW_SOURCE_ICONS: Record = { custom: , }; -const EventRow = React.forwardRef( - ( - { - dataIndex, - event, - isHighlighted, - onClick, - onTimeClick, - }: { - dataIndex: number; - event: SessionEvent; - isHighlighted: boolean; - onClick: VoidFunction; - onTimeClick: VoidFunction; - }, - ref: React.Ref, - ) => { - return ( -
-
- {EVENT_ROW_SOURCE_ICONS[event.eventSource] || ( - - )} -
-
-
- {event.title}{' '} - {event.duration > 0 && {event.duration}ms} -
-
- {event.description} -
+const EventRow = ({ + dataIndex, + event, + isHighlighted, + onClick, + onTimeClick, + ref, +}: { + dataIndex: number; + event: SessionEvent; + isHighlighted: boolean; + onClick: VoidFunction; + onTimeClick: VoidFunction; + ref?: React.Ref; +}) => { + return ( +
+
+ {EVENT_ROW_SOURCE_ICONS[event.eventSource] || ( + + )} +
+
+
+ {event.title} {event.duration > 0 && {event.duration}ms}
-
- - {event.formattedTimestamp} +
+ {event.description}
- ); - }, -); +
+ + {event.formattedTimestamp} +
+
+ ); +}; export const SessionEventList = ({ queriedConfig, diff --git a/packages/app/src/chartSync.tsx b/packages/app/src/chartSync.tsx index 7a6028c761..76f541fb9a 100644 --- a/packages/app/src/chartSync.tsx +++ b/packages/app/src/chartSync.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useId } from 'react'; +import { createContext, use, useId } from 'react'; /** * The default recharts `syncId` group. Charts sharing a syncId cross-highlight @@ -14,7 +14,7 @@ const DEFAULT_CHART_SYNC_ID = 'hdx'; const ChartSyncContext = createContext(DEFAULT_CHART_SYNC_ID); export function useChartSyncId() { - return useContext(ChartSyncContext); + return use(ChartSyncContext); } /** @@ -28,9 +28,5 @@ export function IsolatedChartSyncProvider({ children: React.ReactNode; }) { const syncId = useId(); - return ( - - {children} - - ); + return {children}; } diff --git a/packages/app/src/components/AppNav/AppNav.components.tsx b/packages/app/src/components/AppNav/AppNav.components.tsx index 95344a749c..e20c901316 100644 --- a/packages/app/src/components/AppNav/AppNav.components.tsx +++ b/packages/app/src/components/AppNav/AppNav.components.tsx @@ -86,7 +86,7 @@ export const AppNavUserMenu = ({ logoutUrl, onClickUserPreferences, }: AppNavUserMenuProps) => { - const { isCollapsed } = React.useContext(AppNavContext); + const { isCollapsed } = React.use(AppNavContext); const resolvedUserName = userName.trim() || 'User'; const initials = getUserInitials(resolvedUserName); @@ -181,7 +181,7 @@ export const AppNavUserMenu = ({ }; export const AppNavHelpMenu = ({ version }: { version?: string }) => { - const { isCollapsed } = React.useContext(AppNavContext); + const { isCollapsed } = React.use(AppNavContext); const [ shortcutsOpened, { open: openShortcutsModal, close: closeShortcutsModal }, @@ -294,7 +294,7 @@ export const AppNavLink = ({ isBeta?: boolean; isActive?: boolean; }) => { - const { pathname, isCollapsed } = React.useContext(AppNavContext); + const { pathname, isCollapsed } = React.use(AppNavContext); const testId = `nav-link-${href.replace(/^\//, '').replace(/\//g, '-') || 'home'}`; diff --git a/packages/app/src/components/AppNav/AppNav.tsx b/packages/app/src/components/AppNav/AppNav.tsx index 13dc89e695..32b7d78f16 100644 --- a/packages/app/src/components/AppNav/AppNav.tsx +++ b/packages/app/src/components/AppNav/AppNav.tsx @@ -321,7 +321,7 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) { ]); return ( - + {fixed && (
- + ); } diff --git a/packages/app/src/components/AppNav/AppNavFeedback.tsx b/packages/app/src/components/AppNav/AppNavFeedback.tsx index ca23e5ea34..0109dceef0 100644 --- a/packages/app/src/components/AppNav/AppNavFeedback.tsx +++ b/packages/app/src/components/AppNav/AppNavFeedback.tsx @@ -45,7 +45,7 @@ export const AppNavFeedback = () => ( ); const AppNavFeedbackInner = () => { - const { isCollapsed } = React.useContext(AppNavContext); + const { isCollapsed } = React.use(AppNavContext); const [forceEnabled] = useLocalStorage({ key: FORCE_ENABLE_KEY, defaultValue: false, diff --git a/packages/app/src/components/ContextSidePanel.tsx b/packages/app/src/components/ContextSidePanel.tsx index 60d5312440..f7568ea69a 100644 --- a/packages/app/src/components/ContextSidePanel.tsx +++ b/packages/app/src/components/ContextSidePanel.tsx @@ -1,4 +1,4 @@ -import { useCallback, useContext, useMemo, useState } from 'react'; +import { use, useCallback, useMemo, useState } from 'react'; import ms from 'ms'; import { useForm, useWatch } from 'react-hook-form'; import { tcFromSource } from '@hyperdx/common-utils/dist/core/metadata'; @@ -72,7 +72,7 @@ export default function ContextSubpanel({ const formWhere = useWatch({ control, name: 'where' }); const [debouncedWhere] = useDebouncedValue(formWhere, 1000); - const { setChildModalOpen } = useContext(RowSidePanelContext); + const { setChildModalOpen } = use(RowSidePanelContext); const handleRowExpandClick = useCallback( (rowWhere: RowWhereResult, row: Record) => { diff --git a/packages/app/src/components/DBHighlightedAttributesList.tsx b/packages/app/src/components/DBHighlightedAttributesList.tsx index a32bd49c2c..90c01f64de 100644 --- a/packages/app/src/components/DBHighlightedAttributesList.tsx +++ b/packages/app/src/components/DBHighlightedAttributesList.tsx @@ -1,4 +1,4 @@ -import { useContext, useMemo, useState } from 'react'; +import { use, useMemo, useState } from 'react'; import { TSource } from '@hyperdx/common-utils/dist/types'; import { Anchor, Flex } from '@mantine/core'; @@ -26,7 +26,7 @@ export function DBHighlightedAttributesList({ onPropertyAddClick, generateSearchUrl, source: contextSource, - } = useContext(RowSidePanelContext); + } = use(RowSidePanelContext); const sortedAttributes = useMemo(() => { return attributes diff --git a/packages/app/src/components/DBRowJsonViewer.test.tsx b/packages/app/src/components/DBRowJsonViewer.test.tsx index 75a97f4d97..32ef25dca2 100644 --- a/packages/app/src/components/DBRowJsonViewer.test.tsx +++ b/packages/app/src/components/DBRowJsonViewer.test.tsx @@ -69,9 +69,9 @@ describe('DBRowJsonViewer', () => { // Helper to render component const renderComponent = (data: any) => { return renderWithMantine( - + - , + , ); }; diff --git a/packages/app/src/components/DBRowJsonViewer.tsx b/packages/app/src/components/DBRowJsonViewer.tsx index 2cefde5bb8..e5c5b6faa4 100644 --- a/packages/app/src/components/DBRowJsonViewer.tsx +++ b/packages/app/src/components/DBRowJsonViewer.tsx @@ -1,4 +1,4 @@ -import { useCallback, useContext, useMemo, useState } from 'react'; +import { use, useCallback, useMemo, useState } from 'react'; import router from 'next/router'; import { useAtom, useAtomValue } from 'jotai'; import { atomWithStorage } from 'jotai/utils'; @@ -360,7 +360,7 @@ export function DBRowJsonViewer({ generateChartUrl, displayedColumns, toggleColumn, - } = useContext(RowSidePanelContext); + } = use(RowSidePanelContext); const [filter, setFilter] = useState(''); const [debouncedFilter] = useDebouncedValue(filter, 100); diff --git a/packages/app/src/components/DBRowOverviewPanel.tsx b/packages/app/src/components/DBRowOverviewPanel.tsx index 65f216e575..31fc1906c6 100644 --- a/packages/app/src/components/DBRowOverviewPanel.tsx +++ b/packages/app/src/components/DBRowOverviewPanel.tsx @@ -1,4 +1,4 @@ -import { useCallback, useContext, useMemo } from 'react'; +import { use, useCallback, useMemo } from 'react'; import isString from 'lodash/isString'; import pickBy from 'lodash/pickBy'; import { SourceKind, TSource } from '@hyperdx/common-utils/dist/types'; @@ -45,7 +45,7 @@ export function RowOverviewPanel({ const contentPx = flush ? 0 : 'md'; const { data } = useRowData({ source, rowId, aliasWith, dateRange }); const { onPropertyAddClick, generateSearchUrl, onOpenLinkedTrace } = - useContext(RowSidePanelContext); + use(RowSidePanelContext); const highlightedAttributeValues = useMemo(() => { const attributeExpressions = diff --git a/packages/app/src/components/DBRowSidePanel.tsx b/packages/app/src/components/DBRowSidePanel.tsx index bf90e75a0d..55ad59fa5a 100644 --- a/packages/app/src/components/DBRowSidePanel.tsx +++ b/packages/app/src/components/DBRowSidePanel.tsx @@ -1,7 +1,7 @@ import { createContext, + use, useCallback, - useContext, useEffect, useMemo, useRef, @@ -338,7 +338,7 @@ export const DBRowSidePanelInner = ({ const hasActiveStacks = activeSourceFrame != null || leafNav != null; - const parentContext = useContext(RowSidePanelContext); + const parentContext = use(RowSidePanelContext); // Nested rows shouldn't inherit the parent table's row config. const dbSqlRowTableConfig = hasActiveStacks ? undefined diff --git a/packages/app/src/components/DBRowTable.tsx b/packages/app/src/components/DBRowTable.tsx index 19574d849b..5e0951b375 100644 --- a/packages/app/src/components/DBRowTable.tsx +++ b/packages/app/src/components/DBRowTable.tsx @@ -1,7 +1,7 @@ import React, { memo, + use, useCallback, - useContext, useEffect, useMemo, useState, @@ -1559,7 +1559,7 @@ function DBSqlRowTableComponent({ }) { const { data: me } = api.useMe(); const { toggleColumn, displayedColumns: contextDisplayedColumns } = - useContext(RowSidePanelContext); + use(RowSidePanelContext); const [orderBy, setOrderBy] = useState( initialSortBy?.[0] ?? null, diff --git a/packages/app/src/components/DBSqlRowTableWithSidebar.tsx b/packages/app/src/components/DBSqlRowTableWithSidebar.tsx index bd5824e372..95fe214f41 100644 --- a/packages/app/src/components/DBSqlRowTableWithSidebar.tsx +++ b/packages/app/src/components/DBSqlRowTableWithSidebar.tsx @@ -115,7 +115,7 @@ export default function DBSqlRowTableWithSideBar({ ); return ( - + {sourceData && (rowSource === sourceId || !rowSource) && ( - + ); } diff --git a/packages/app/src/components/DBTable/DBRowTableFieldWithPopover.tsx b/packages/app/src/components/DBTable/DBRowTableFieldWithPopover.tsx index d25123c04e..f8a966e2ed 100644 --- a/packages/app/src/components/DBTable/DBRowTableFieldWithPopover.tsx +++ b/packages/app/src/components/DBTable/DBRowTableFieldWithPopover.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useRef, useState } from 'react'; +import React, { use, useEffect, useRef, useState } from 'react'; import cx from 'classnames'; import { Popover } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; @@ -51,7 +51,7 @@ const DBRowTableFieldWithPopover = ({ }, []); // Get filter functionality from context - const { onPropertyAddClick } = useContext(RowSidePanelContext); + const { onPropertyAddClick } = use(RowSidePanelContext); // Check if we have both the column name and filter function available const canFilter = columnName && onPropertyAddClick && cellValue != null; diff --git a/packages/app/src/components/FullscreenPanelModal.tsx b/packages/app/src/components/FullscreenPanelModal.tsx index 8977ac9a79..7da131e172 100644 --- a/packages/app/src/components/FullscreenPanelModal.tsx +++ b/packages/app/src/components/FullscreenPanelModal.tsx @@ -53,7 +53,7 @@ export default function FullscreenPanelModal({ trapFocus={false} lockScroll > - + {/* Isolate chart cross-syncing to this modal: a chart shown fullscreen should not drive shadow tooltips on the dashboard tiles behind it (which now render over the modal). */} @@ -70,7 +70,7 @@ export default function FullscreenPanelModal({ {children} - + ); } diff --git a/packages/app/src/components/HyperJson.tsx b/packages/app/src/components/HyperJson.tsx index 8a6a51aaf7..f7098eb939 100644 --- a/packages/app/src/components/HyperJson.tsx +++ b/packages/app/src/components/HyperJson.tsx @@ -52,7 +52,7 @@ const hyperJsonAtom = atom({ }); const ValueRenderer = React.memo( - React.forwardRef(({ value }, ref) => { + ({ value, ref }: { value: any; ref?: React.Ref }) => { if (isNull(value)) { return ( @@ -96,7 +96,7 @@ const ValueRenderer = React.memo( ); } return null; - }), + }, ); const LineMenu = React.memo( diff --git a/packages/app/src/components/PatternSidePanel.tsx b/packages/app/src/components/PatternSidePanel.tsx index 0b52b8c26c..867e5939eb 100644 --- a/packages/app/src/components/PatternSidePanel.tsx +++ b/packages/app/src/components/PatternSidePanel.tsx @@ -127,7 +127,7 @@ export default function PatternSidePanel({ }, }} > - +
- + ); } diff --git a/packages/app/src/components/ServiceMap/ServiceMap.tsx b/packages/app/src/components/ServiceMap/ServiceMap.tsx index f1bc3fea92..e8a5f3a6ce 100644 --- a/packages/app/src/components/ServiceMap/ServiceMap.tsx +++ b/packages/app/src/components/ServiceMap/ServiceMap.tsx @@ -283,7 +283,7 @@ function ServiceMapPresentation({ return (
- + - +
); } diff --git a/packages/app/src/components/ServiceMap/ServiceMapNode.tsx b/packages/app/src/components/ServiceMap/ServiceMapNode.tsx index 6a9fbed2b8..37308b1ecc 100644 --- a/packages/app/src/components/ServiceMap/ServiceMapNode.tsx +++ b/packages/app/src/components/ServiceMap/ServiceMapNode.tsx @@ -1,4 +1,4 @@ -import { useContext } from 'react'; +import { use } from 'react'; import { TTraceSource } from '@hyperdx/common-utils/dist/types'; import { Text } from '@mantine/core'; import { Handle, Node, NodeProps, NodeToolbar, Position } from '@xyflow/react'; @@ -53,7 +53,7 @@ export default function ServiceMapNode( onFocusService, } = data; - const { metric, metricMax } = useContext(ServiceMapMetricContext); + const { metric, metricMax } = use(ServiceMapMetricContext); const { backgroundColor, borderColor } = getNodeColors( getServiceMetricValue(data, metric), diff --git a/packages/app/src/components/__tests__/AppNavUserMenu.test.tsx b/packages/app/src/components/__tests__/AppNavUserMenu.test.tsx index 57fbc6214f..0aee4d2178 100644 --- a/packages/app/src/components/__tests__/AppNavUserMenu.test.tsx +++ b/packages/app/src/components/__tests__/AppNavUserMenu.test.tsx @@ -8,9 +8,9 @@ import { const renderAppNavUserMenu = (userName?: string) => { return renderWithMantine( - + - , + , ); }; diff --git a/packages/app/src/components/__tests__/DBTraceWaterfallChart.test.tsx b/packages/app/src/components/__tests__/DBTraceWaterfallChart.test.tsx index b689f9f87a..f2fecf6a12 100644 --- a/packages/app/src/components/__tests__/DBTraceWaterfallChart.test.tsx +++ b/packages/app/src/components/__tests__/DBTraceWaterfallChart.test.tsx @@ -200,7 +200,7 @@ describe('DBTraceWaterfallChartContainer', () => { traceId: string = mockTraceId, ) => { return renderWithMantine( - + { dateRange={mockDateRange} focusDate={mockFocusDate} /> - , + , ); }; diff --git a/packages/app/src/theme/ThemeProvider.tsx b/packages/app/src/theme/ThemeProvider.tsx index 435ff26d9e..071601dfa9 100644 --- a/packages/app/src/theme/ThemeProvider.tsx +++ b/packages/app/src/theme/ThemeProvider.tsx @@ -1,7 +1,7 @@ import React, { createContext, + use, useCallback, - useContext, useEffect, useMemo, useRef, @@ -178,15 +178,11 @@ export function AppThemeProvider({ }; }, [theme, setTheme, toggleTheme, clearThemeOverride]); - return ( - - {children} - - ); + return {children}; } export function useAppTheme(): ThemeContextValue { - const context = useContext(ThemeContext); + const context = use(ThemeContext); if (!context) { // Fallback for when used outside provider - always use default to avoid hydration issues const theme = getTheme(DEFAULT_THEME); diff --git a/packages/app/src/useConfirm.tsx b/packages/app/src/useConfirm.tsx index f7e2319273..f84374c0f7 100644 --- a/packages/app/src/useConfirm.tsx +++ b/packages/app/src/useConfirm.tsx @@ -63,7 +63,7 @@ export function ConfirmProvider({ children }: { children: React.ReactNode }) { ); return ( - + {children} - + ); } export const useConfirm = () => { - const confirm = React.useContext(ConfirmContext); + const confirm = React.use(ConfirmContext); if (confirm == null) { throw new Error('useConfirm must be used within a ConfirmProvider'); } diff --git a/packages/app/src/zIndex.ts b/packages/app/src/zIndex.ts index 254bfaf1a0..f3e8958ec9 100644 --- a/packages/app/src/zIndex.ts +++ b/packages/app/src/zIndex.ts @@ -1,8 +1,8 @@ -import { createContext, useContext } from 'react'; +import { createContext, use } from 'react'; export const ZIndexContext = createContext(0); export function useZIndex() { - const zIndex = useContext(ZIndexContext); + const zIndex = use(ZIndexContext); return zIndex; }