mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
fix: add react-hooks eslint to catch pitfalls (#1661)
Had a couple of files with large fixes, I tested AutocompleteInput.tsx and SessionSubpanel.tsx quite a bit. Tested most others as well. I didn't act on some due to either a) correct usage or b) they are stable and I don't want to alter it. In both those cases, I added `eslint-disable-next-line` before each relevant line References HDX-2955
This commit is contained in:
parent
b6c34b13d9
commit
e11b313807
34 changed files with 232 additions and 225 deletions
5
.changeset/rotten-kings-rhyme.md
Normal file
5
.changeset/rotten-kings-rhyme.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@hyperdx/app": patch
|
||||
---
|
||||
|
||||
fix: add react-hooks-eslint-plugin and fix issues across app
|
||||
|
|
@ -50,6 +50,8 @@ export default [
|
|||
rules: {
|
||||
...nextPlugin.configs.recommended.rules,
|
||||
...nextPlugin.configs['core-web-vitals'].rules,
|
||||
...reactHooksPlugin.configs.recommended.rules,
|
||||
'react-hooks/set-state-in-effect': 'warn',
|
||||
'@typescript-eslint/ban-ts-comment': 'warn',
|
||||
'@typescript-eslint/no-empty-function': 'warn',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@
|
|||
"eslint-config-next": "^16.0.10",
|
||||
"eslint-plugin-playwright": "^2.4.0",
|
||||
"eslint-plugin-react-hook-form": "^0.3.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-storybook": "10.1.4",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^30.2.0",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import Fuse from 'fuse.js';
|
||||
import { Popover, Textarea, UnstyledButton } from '@mantine/core';
|
||||
|
||||
|
|
@ -46,9 +46,19 @@ export default function AutocompleteInput({
|
|||
}) {
|
||||
const suggestionsLimit = 10;
|
||||
|
||||
const [isSearchInputFocused, setIsSearchInputFocused] = useState(false);
|
||||
const [isSearchInputFocused, _setIsSearchInputFocused] = useState(false);
|
||||
const [isInputDropdownOpen, setIsInputDropdownOpen] = useState(false);
|
||||
const [showSearchHistory, setShowSearchHistory] = useState(false);
|
||||
const setIsSearchInputFocused = useCallback(
|
||||
(state: boolean) => {
|
||||
_setIsSearchInputFocused(state);
|
||||
setIsInputDropdownOpen(state);
|
||||
},
|
||||
[_setIsSearchInputFocused],
|
||||
);
|
||||
const [rightSectionWidth, setRightSectionWidth] = useState<number | 'auto'>(
|
||||
'auto',
|
||||
);
|
||||
const [inputWidth, setInputWidth] = useState<number>(720);
|
||||
|
||||
const [selectedAutocompleteIndex, setSelectedAutocompleteIndex] =
|
||||
useState(-1);
|
||||
|
|
@ -67,25 +77,11 @@ export default function AutocompleteInput({
|
|||
});
|
||||
}, [queryHistory, queryHistoryType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSearchInputFocused) {
|
||||
setIsInputDropdownOpen(true);
|
||||
}
|
||||
}, [isSearchInputFocused]);
|
||||
|
||||
useEffect(() => {
|
||||
// only show search history when: 1.no input, 2.has search type, 3.has history list
|
||||
if (
|
||||
value != null &&
|
||||
value.length === 0 &&
|
||||
queryHistoryList.length > 0 &&
|
||||
queryHistoryType
|
||||
) {
|
||||
setShowSearchHistory(true);
|
||||
} else {
|
||||
setShowSearchHistory(false);
|
||||
}
|
||||
}, [value, queryHistoryType, queryHistoryList]);
|
||||
const showSearchHistory =
|
||||
value != null &&
|
||||
value.length === 0 &&
|
||||
queryHistoryList.length > 0 &&
|
||||
queryHistoryType;
|
||||
|
||||
const fuse = useMemo(
|
||||
() =>
|
||||
|
|
@ -128,6 +124,14 @@ export default function AutocompleteInput({
|
|||
inputRef.current?.focus();
|
||||
};
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useLayoutEffect(() => {
|
||||
if (ref.current) {
|
||||
setRightSectionWidth(ref.current.clientWidth);
|
||||
}
|
||||
if (inputRef.current) {
|
||||
setInputWidth(inputRef.current.clientWidth);
|
||||
}
|
||||
}, [language, onLanguageChange, inputRef]);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
|
|
@ -141,10 +145,7 @@ export default function AutocompleteInput({
|
|||
closeOnEscape
|
||||
styles={{
|
||||
dropdown: {
|
||||
maxWidth:
|
||||
(inputRef.current?.clientWidth || 0) > 300
|
||||
? inputRef.current?.clientWidth
|
||||
: 720,
|
||||
maxWidth: inputWidth > 300 ? inputWidth : 720,
|
||||
width: '100%',
|
||||
zIndex,
|
||||
},
|
||||
|
|
@ -242,7 +243,7 @@ export default function AutocompleteInput({
|
|||
}
|
||||
}
|
||||
}}
|
||||
rightSectionWidth={ref.current?.clientWidth ?? 'auto'}
|
||||
rightSectionWidth={rightSectionWidth}
|
||||
rightSection={
|
||||
language != null && onLanguageChange != null ? (
|
||||
<div ref={ref}>
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ function AIAssistant({
|
|||
</Group>
|
||||
<Collapse in={opened}>
|
||||
{opened && (
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Group mb="md">
|
||||
<SourceSelectControlled
|
||||
|
|
|
|||
|
|
@ -764,19 +764,16 @@ export function useDefaultOrderBy(sourceID: string | undefined | null) {
|
|||
const { data: source } = useSource({ id: sourceID });
|
||||
const { data: tableMetadata } = useTableMetadata(tcFromSource(source));
|
||||
|
||||
// If no source, return undefined so that the orderBy is not set incorrectly
|
||||
if (!source) return undefined;
|
||||
|
||||
// When source changes, make sure select and orderby fields are set to default
|
||||
return useMemo(
|
||||
() =>
|
||||
optimizeDefaultOrderBy(
|
||||
source?.timestampValueExpression ?? '',
|
||||
source?.displayedTimestampValueExpression,
|
||||
tableMetadata?.sorting_key,
|
||||
),
|
||||
[source, tableMetadata],
|
||||
);
|
||||
return useMemo(() => {
|
||||
// If no source, return undefined so that the orderBy is not set incorrectly
|
||||
if (!source) return undefined;
|
||||
return optimizeDefaultOrderBy(
|
||||
source?.timestampValueExpression ?? '',
|
||||
source?.displayedTimestampValueExpression,
|
||||
tableMetadata?.sorting_key,
|
||||
);
|
||||
}, [source, tableMetadata]);
|
||||
}
|
||||
|
||||
// This is outside as it needs to be a stable reference
|
||||
|
|
|
|||
|
|
@ -264,6 +264,7 @@ export default function DOMPlayer({
|
|||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/immutability
|
||||
updatePlayerTimeRafRef.current = requestAnimationFrame(updatePlayerTime);
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export default function LandingHeader({
|
|||
activeKey: string;
|
||||
fixed?: boolean;
|
||||
}) {
|
||||
const Wordmark = useWordmark();
|
||||
const wordmark = useWordmark();
|
||||
const { data: me } = api.useMe();
|
||||
const isLoggedIn = Boolean(me);
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ export default function LandingHeader({
|
|||
<Container fluid px="xl" py="md">
|
||||
<Group justify="space-between" align="center">
|
||||
<Link href="/" style={{ textDecoration: 'none' }}>
|
||||
<Wordmark />
|
||||
{wordmark}
|
||||
</Link>
|
||||
|
||||
<Burger
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import {
|
|||
} from '@tabler/icons-react';
|
||||
|
||||
import { useQueriedChartConfig } from './hooks/useChartConfig';
|
||||
import { useLogomark } from './theme/ThemeProvider';
|
||||
import api from './api';
|
||||
import { useConnections } from './connection';
|
||||
import { useSources } from './source';
|
||||
|
|
@ -36,12 +35,12 @@ interface OnboardingStep {
|
|||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const NOW = Date.now();
|
||||
const OnboardingChecklist = ({
|
||||
onAddDataClick,
|
||||
}: {
|
||||
onAddDataClick?: () => void;
|
||||
}) => {
|
||||
const Logomark = useLogomark();
|
||||
const [isCollapsed, setIsCollapsed] = useLocalStorage(
|
||||
'onboardingChecklistCollapsed',
|
||||
false,
|
||||
|
|
@ -55,7 +54,7 @@ const OnboardingChecklist = ({
|
|||
// Check if team is new (less than 3 days old)
|
||||
const isNewTeam = useMemo(() => {
|
||||
if (!team?.createdAt) return false;
|
||||
const threeDaysAgo = new Date(Date.now() - 1000 * 60 * 60 * 24 * 3);
|
||||
const threeDaysAgo = new Date(NOW - 1000 * 60 * 60 * 24 * 3);
|
||||
return new Date(team.createdAt) > threeDaysAgo;
|
||||
}, [team]);
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
Button,
|
||||
Divider,
|
||||
Group,
|
||||
Portal,
|
||||
SegmentedControl,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
|
|
@ -66,16 +67,10 @@ function useSessionChartConfigs({
|
|||
const { getFieldExpression: getTraceSourceFieldExpression } =
|
||||
useFieldExpressionGenerator(traceSource);
|
||||
|
||||
if (!getTraceSourceFieldExpression) {
|
||||
return {
|
||||
eventsConfig: undefined,
|
||||
aliasMap: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// Should produce rows that match the `sessionRowSchema` in packages/app/src/utils/sessions.ts
|
||||
const select = useMemo(
|
||||
() => [
|
||||
const select = useMemo(() => {
|
||||
if (!getTraceSourceFieldExpression) return [];
|
||||
return [
|
||||
{
|
||||
valueExpression: `${getTraceSourceFieldExpression(traceSource.eventAttributesExpression ?? 'SpanAttributes', 'message')}`,
|
||||
alias: 'body',
|
||||
|
|
@ -145,9 +140,8 @@ function useSessionChartConfigs({
|
|||
valueExpression: `CAST('span', 'String')`,
|
||||
alias: 'type',
|
||||
},
|
||||
],
|
||||
[traceSource, getTraceSourceFieldExpression],
|
||||
);
|
||||
];
|
||||
}, [traceSource, getTraceSourceFieldExpression]);
|
||||
|
||||
// Events shown in the highlighted tab
|
||||
const highlightedEventsFilter = useMemo(
|
||||
|
|
@ -166,8 +160,9 @@ function useSessionChartConfigs({
|
|||
[traceSource, rumSessionId],
|
||||
);
|
||||
|
||||
const allEventsFilter = useMemo(
|
||||
() => ({
|
||||
const allEventsFilter = useMemo(() => {
|
||||
if (!getTraceSourceFieldExpression) return undefined;
|
||||
return {
|
||||
type: 'lucene' as const,
|
||||
condition: `${traceSource.resourceAttributesExpression}.rum.sessionId:"${rumSessionId}"
|
||||
AND (
|
||||
|
|
@ -179,12 +174,13 @@ function useSessionChartConfigs({
|
|||
OR ${traceSource.spanNameExpression}:"intercom.onShow"
|
||||
OR ScopeName:"custom-action"
|
||||
)`,
|
||||
}),
|
||||
[traceSource, rumSessionId],
|
||||
);
|
||||
};
|
||||
}, [traceSource, rumSessionId, getTraceSourceFieldExpression]);
|
||||
|
||||
const eventsConfig = useMemo<ChartConfigWithOptDateRange>(
|
||||
() => ({
|
||||
const eventsConfig = useMemo<ChartConfigWithOptDateRange | undefined>(() => {
|
||||
if (!getTraceSourceFieldExpression || !select || !allEventsFilter)
|
||||
return undefined;
|
||||
return {
|
||||
select: select,
|
||||
from: traceSource.from,
|
||||
dateRange: [start, end],
|
||||
|
|
@ -201,21 +197,22 @@ function useSessionChartConfigs({
|
|||
filters: [
|
||||
tab === 'highlighted' ? highlightedEventsFilter : allEventsFilter,
|
||||
],
|
||||
}),
|
||||
[
|
||||
select,
|
||||
traceSource,
|
||||
start,
|
||||
end,
|
||||
where,
|
||||
whereLanguage,
|
||||
tab,
|
||||
highlightedEventsFilter,
|
||||
allEventsFilter,
|
||||
],
|
||||
);
|
||||
};
|
||||
}, [
|
||||
select,
|
||||
traceSource,
|
||||
start,
|
||||
end,
|
||||
where,
|
||||
whereLanguage,
|
||||
tab,
|
||||
highlightedEventsFilter,
|
||||
allEventsFilter,
|
||||
getTraceSourceFieldExpression,
|
||||
]);
|
||||
|
||||
const aliasMap = useMemo(() => {
|
||||
if (!getTraceSourceFieldExpression) return undefined;
|
||||
// valueExpression: alias
|
||||
return select.reduce(
|
||||
(acc, { valueExpression, alias }) => {
|
||||
|
|
@ -224,7 +221,7 @@ function useSessionChartConfigs({
|
|||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
}, [select]);
|
||||
}, [select, getTraceSourceFieldExpression]);
|
||||
|
||||
return {
|
||||
eventsConfig,
|
||||
|
|
@ -269,46 +266,23 @@ export default function SessionSubpanel({
|
|||
const [rowId, setRowId] = useState<string | undefined>(undefined);
|
||||
const [aliasWith, setAliasWith] = useState<WithClause[]>([]);
|
||||
|
||||
// Without portaling the nested drawer close overlay will not render properly
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
useEffect(() => {
|
||||
containerRef.current = document.createElement('div');
|
||||
|
||||
if (containerRef.current) {
|
||||
document.body.appendChild(containerRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (containerRef.current) {
|
||||
document.body.removeChild(containerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const portaledPanel =
|
||||
containerRef.current != null
|
||||
? ReactDOM.createPortal(
|
||||
traceSource && (
|
||||
<DBRowSidePanel
|
||||
source={traceSource}
|
||||
rowId={rowId}
|
||||
aliasWith={aliasWith}
|
||||
onClose={() => {
|
||||
setDrawerOpen(false);
|
||||
setRowId(undefined);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
containerRef.current,
|
||||
)
|
||||
: null;
|
||||
|
||||
const [tsQuery, setTsQuery] = useQueryState(
|
||||
'ts',
|
||||
parseAsInteger.withOptions({ history: 'replace' }),
|
||||
);
|
||||
const prevTsQuery = usePrevious(tsQuery);
|
||||
|
||||
const [focus, _setFocus] = useState<
|
||||
{ ts: number; setBy: string } | undefined
|
||||
>(
|
||||
initialTs != null
|
||||
? {
|
||||
ts: initialTs,
|
||||
setBy: 'parent',
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (prevTsQuery == null && tsQuery != null) {
|
||||
_setFocus({ ts: tsQuery, setBy: 'url' });
|
||||
|
|
@ -327,17 +301,6 @@ export default function SessionSubpanel({
|
|||
};
|
||||
}, [setTsQuery]);
|
||||
|
||||
const [focus, _setFocus] = useState<
|
||||
{ ts: number; setBy: string } | undefined
|
||||
>(
|
||||
initialTs != null
|
||||
? {
|
||||
ts: initialTs,
|
||||
setBy: 'parent',
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
|
||||
const setFocus = useCallback(
|
||||
(focus: { ts: number; setBy: string }) => {
|
||||
if (focus.setBy === 'player') {
|
||||
|
|
@ -471,10 +434,44 @@ export default function SessionSubpanel({
|
|||
},
|
||||
[setSearchedQuery],
|
||||
);
|
||||
const onSessionEventClick = useCallback(
|
||||
(rowWhere: RowWhereResult) => {
|
||||
setDrawerOpen(true);
|
||||
setRowId(rowWhere.where);
|
||||
setAliasWith(rowWhere.aliasWith);
|
||||
},
|
||||
[setDrawerOpen, setRowId, setAliasWith],
|
||||
);
|
||||
const onSessionEventTimeClick = useCallback(
|
||||
(ts: number) => {
|
||||
setFocus({ ts, setBy: 'timeline' });
|
||||
},
|
||||
[setFocus],
|
||||
);
|
||||
const setPlayerTime = useCallback(
|
||||
(ts: number) => {
|
||||
if (focus?.setBy !== 'player' || focus?.ts !== ts) {
|
||||
setFocus({ ts, setBy: 'player' });
|
||||
}
|
||||
},
|
||||
[focus, setFocus],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
{rowId != null && portaledPanel}
|
||||
{rowId != null && traceSource && (
|
||||
<Portal>
|
||||
<DBRowSidePanel
|
||||
source={traceSource}
|
||||
rowId={rowId}
|
||||
aliasWith={aliasWith}
|
||||
onClose={() => {
|
||||
setDrawerOpen(false);
|
||||
setRowId(undefined);
|
||||
}}
|
||||
/>
|
||||
</Portal>
|
||||
)}
|
||||
<div className={cx(styles.eventList, { 'd-none': playerFullWidth })}>
|
||||
<div className={styles.eventListHeader}>
|
||||
<form
|
||||
|
|
@ -536,21 +533,9 @@ export default function SessionSubpanel({
|
|||
eventsFollowPlayerPosition={eventsFollowPlayerPosition}
|
||||
aliasMap={aliasMap}
|
||||
queriedConfig={eventsConfig}
|
||||
onClick={useCallback(
|
||||
(rowWhere: RowWhereResult) => {
|
||||
setDrawerOpen(true);
|
||||
setRowId(rowWhere.where);
|
||||
setAliasWith(rowWhere.aliasWith);
|
||||
},
|
||||
[setDrawerOpen, setRowId, setAliasWith],
|
||||
)}
|
||||
onClick={onSessionEventClick}
|
||||
focus={focus}
|
||||
onTimeClick={useCallback(
|
||||
ts => {
|
||||
setFocus({ ts, setBy: 'timeline' });
|
||||
},
|
||||
[setFocus],
|
||||
)}
|
||||
onTimeClick={onSessionEventTimeClick}
|
||||
minTs={minTs}
|
||||
showRelativeTime={showRelativeTime}
|
||||
/>
|
||||
|
|
@ -563,14 +548,7 @@ export default function SessionSubpanel({
|
|||
playerState={playerState}
|
||||
setPlayerState={setPlayerState}
|
||||
focus={focus}
|
||||
setPlayerTime={useCallback(
|
||||
ts => {
|
||||
if (focus?.setBy !== 'player' || focus?.ts !== ts) {
|
||||
setFocus({ ts, setBy: 'player' });
|
||||
}
|
||||
},
|
||||
[focus, setFocus],
|
||||
)}
|
||||
setPlayerTime={setPlayerTime}
|
||||
config={{
|
||||
serviceName: session.serviceName,
|
||||
sourceId: sessionSource.id,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import '@mantine/spotlight/styles.css';
|
|||
|
||||
export const useSpotlightActions = () => {
|
||||
const router = useRouter();
|
||||
const Logomark = useLogomark();
|
||||
const logomark = useLogomark({ size: 16 });
|
||||
|
||||
const { data: logViewsData } = useSavedSearches();
|
||||
const { data: dashboardsData } = api.useDashboards();
|
||||
|
|
@ -151,7 +151,7 @@ export const useSpotlightActions = () => {
|
|||
{
|
||||
id: 'cloud',
|
||||
group: 'Menu',
|
||||
leftSection: <Logomark size={16} />,
|
||||
leftSection: logomark,
|
||||
label: 'HyperDX Cloud',
|
||||
description: 'Ready to use HyperDX Cloud? Get started for free.',
|
||||
keywords: ['account', 'profile'],
|
||||
|
|
@ -162,7 +162,7 @@ export const useSpotlightActions = () => {
|
|||
);
|
||||
|
||||
return logViewActions;
|
||||
}, [Logomark, logViewsData, dashboardsData, router]);
|
||||
}, [logomark, logViewsData, dashboardsData, router]);
|
||||
|
||||
return { actions };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -391,8 +391,8 @@ function useSearchableList<T extends AppNavLinkItem>({
|
|||
}
|
||||
|
||||
export default function AppNav({ fixed = false }: { fixed?: boolean }) {
|
||||
const Wordmark = useWordmark();
|
||||
const Logomark = useLogomark();
|
||||
const wordmark = useWordmark();
|
||||
const logomark = useLogomark({ size: 22 });
|
||||
|
||||
useEffect(() => {
|
||||
let redirectUrl;
|
||||
|
|
@ -676,12 +676,10 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
|
|||
>
|
||||
<Link href="/search" className={styles.logoLink}>
|
||||
{isCollapsed ? (
|
||||
<div className={styles.logoIconWrapper}>
|
||||
<Logomark size={22} />
|
||||
</div>
|
||||
<div className={styles.logoIconWrapper}>{logomark}</div>
|
||||
) : (
|
||||
<Group gap="xs" align="center">
|
||||
<Wordmark />
|
||||
{wordmark}
|
||||
{isUTC && (
|
||||
<Badge
|
||||
size="xs"
|
||||
|
|
|
|||
|
|
@ -168,13 +168,11 @@ export function DBRowJsonViewer({
|
|||
}
|
||||
|
||||
// remove internal aliases (keys that start with __hdx_)
|
||||
Object.keys(data).forEach(key => {
|
||||
if (key.startsWith('__hdx_')) {
|
||||
delete data[key];
|
||||
}
|
||||
});
|
||||
const cleanedData = Object.fromEntries(
|
||||
Object.entries(data).filter(entry => !entry[0].startsWith('__hdx_')),
|
||||
);
|
||||
|
||||
return filterObjectRecursively(data, debouncedFilter);
|
||||
return filterObjectRecursively(cleanedData, debouncedFilter);
|
||||
}, [data, debouncedFilter]);
|
||||
|
||||
const getLineActions = useCallback<GetLineActions>(
|
||||
|
|
|
|||
|
|
@ -423,7 +423,19 @@ export const RawLogTable = memo(
|
|||
const FETCH_NEXT_PAGE_PX = 500;
|
||||
|
||||
//we need a reference to the scrolling element for logic down below
|
||||
const tableContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [tableContainerRef, setTableContainerRef] =
|
||||
useState<HTMLDivElement | null>(null);
|
||||
const tableContainerRefCallback = useCallback(
|
||||
(node: HTMLDivElement): (() => void) => {
|
||||
if (node) {
|
||||
setTableContainerRef(node);
|
||||
}
|
||||
return () => {
|
||||
setTableContainerRef(null);
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Get the alias map from the config so we resolve correct column ids
|
||||
const { data: aliasMap } = useAliasMapFromChartConfig(config);
|
||||
|
|
@ -431,10 +443,10 @@ export const RawLogTable = memo(
|
|||
// Reset scroll when live tail is enabled for the first time
|
||||
const prevIsLive = usePrevious(isLive);
|
||||
useEffect(() => {
|
||||
if (isLive && prevIsLive === false && tableContainerRef.current != null) {
|
||||
tableContainerRef.current.scrollTop = 0;
|
||||
if (isLive && prevIsLive === false && tableContainerRef != null) {
|
||||
tableContainerRef.scrollTop = 0;
|
||||
}
|
||||
}, [isLive, prevIsLive]);
|
||||
}, [isLive, prevIsLive, tableContainerRef]);
|
||||
|
||||
const logLevelColumn = useMemo(() => {
|
||||
return inferLogLevelColumn(dedupedRows);
|
||||
|
|
@ -600,8 +612,8 @@ export const RawLogTable = memo(
|
|||
|
||||
//a check on mount and after a fetch to see if the table is already scrolled to the bottom and immediately needs to fetch more data
|
||||
useEffect(() => {
|
||||
fetchMoreOnBottomReached(tableContainerRef.current);
|
||||
}, [fetchMoreOnBottomReached]);
|
||||
fetchMoreOnBottomReached(tableContainerRef);
|
||||
}, [fetchMoreOnBottomReached, tableContainerRef]);
|
||||
|
||||
const reactTableProps = useMemo((): TableOptions<any> => {
|
||||
//TODO: fix any
|
||||
|
|
@ -664,7 +676,7 @@ export const RawLogTable = memo(
|
|||
count: _rows.length,
|
||||
// count: hasNextPage ? allRows.length + 1 : allRows.length,
|
||||
getScrollElement: useCallback(
|
||||
() => tableContainerRef.current,
|
||||
() => tableContainerRef,
|
||||
[tableContainerRef],
|
||||
),
|
||||
estimateSize: useCallback(() => 23, []),
|
||||
|
|
@ -853,7 +865,7 @@ export const RawLogTable = memo(
|
|||
onScroll?.(scrollTop);
|
||||
}
|
||||
}}
|
||||
ref={tableContainerRef}
|
||||
ref={tableContainerRefCallback}
|
||||
// Fixes flickering scroll bar: https://github.com/TanStack/virtual/issues/426#issuecomment-1403438040
|
||||
// style={{ overflowAnchor: 'none' }}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -64,22 +64,19 @@ export const useSessionId = ({
|
|||
});
|
||||
|
||||
const result = useMemo(() => {
|
||||
for (const row of data?.data || []) {
|
||||
if (row.parentSpanId === null && row.rumSessionId) {
|
||||
return {
|
||||
rumServiceName: row.serviceName,
|
||||
rumSessionId: row.rumSessionId,
|
||||
};
|
||||
}
|
||||
const rowData = data?.data || [];
|
||||
let row = rowData.find(
|
||||
row => row.parentSpanId === null && row.rumSessionId,
|
||||
);
|
||||
if (!row) {
|
||||
// otherwise just return the first session id
|
||||
row = rowData.find(row => row.rumSessionId);
|
||||
}
|
||||
// otherwise just return the first session id
|
||||
for (const row of data?.data || []) {
|
||||
if (row.rumSessionId) {
|
||||
return {
|
||||
rumServiceName: row.serviceName,
|
||||
rumSessionId: row.rumSessionId,
|
||||
};
|
||||
}
|
||||
if (row) {
|
||||
return {
|
||||
rumServiceName: row.serviceName,
|
||||
rumSessionId: row.rumSessionId,
|
||||
};
|
||||
}
|
||||
return { rumServiceName: undefined, rumSessionId: undefined };
|
||||
}, [data]);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export interface DBRowTableFieldWithPopoverProps {
|
|||
cellValue: unknown;
|
||||
wrapLinesEnabled: boolean;
|
||||
columnName?: string;
|
||||
tableContainerRef?: React.RefObject<HTMLDivElement | null>;
|
||||
tableContainerRef: HTMLDivElement | null;
|
||||
isChart?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -148,7 +148,7 @@ export const DBRowTableFieldWithPopover = ({
|
|||
position="top-start"
|
||||
offset={5}
|
||||
opened={opened}
|
||||
portalProps={{ target: tableContainerRef?.current ?? undefined }}
|
||||
portalProps={{ target: tableContainerRef ?? undefined }}
|
||||
closeOnClickOutside={false}
|
||||
clickOutsideEvents={[]}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ interface TableSearchInputProps {
|
|||
/**
|
||||
* Reference to a container element to check if it's clickable (not obscured by modal/drawer)
|
||||
*/
|
||||
containerRef?: React.RefObject<HTMLElement | null>;
|
||||
containerRef?: HTMLElement | null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -145,8 +145,7 @@ export const TableSearchInput = ({
|
|||
// Detect Cmd+F (Mac) or Ctrl+F (Windows/Linux)
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'f') {
|
||||
// if container exists, verify it's actually clickable
|
||||
if (containerRef?.current && !isElementClickable(containerRef.current))
|
||||
return;
|
||||
if (containerRef && !isElementClickable(containerRef)) return;
|
||||
|
||||
e.preventDefault();
|
||||
handleShow();
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ export default function DBTableChart({
|
|||
}
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
}, [config?.select]);
|
||||
}, [config.select]);
|
||||
const columns = useMemo(() => {
|
||||
const rows = data?.data ?? [];
|
||||
if (rows.length === 0) {
|
||||
|
|
|
|||
|
|
@ -153,15 +153,12 @@ export const NumberFormatForm: React.FC<{
|
|||
key="numberFormat.factor"
|
||||
name="numberFormat.factor"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
const options = useMemo(
|
||||
() => [
|
||||
{ value: '1', label: 'Seconds' },
|
||||
{ value: '0.001', label: 'Milliseconds' },
|
||||
{ value: '0.000001', label: 'Microseconds' },
|
||||
{ value: '0.000000001', label: 'Nanoseconds' },
|
||||
],
|
||||
[],
|
||||
);
|
||||
const options = [
|
||||
{ value: '1', label: 'Seconds' },
|
||||
{ value: '0.001', label: 'Milliseconds' },
|
||||
{ value: '0.000001', label: 'Microseconds' },
|
||||
{ value: '0.000000001', label: 'Nanoseconds' },
|
||||
];
|
||||
|
||||
const stringValue =
|
||||
options.find(option => parseFloat(option.value) === value)
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ export default function SQLEditor({
|
|||
minHeight={'100px'}
|
||||
extensions={[
|
||||
styleTheme,
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
compartmentRef.current.of(
|
||||
sql({
|
||||
upperCaseKeywords: true,
|
||||
|
|
|
|||
|
|
@ -369,6 +369,7 @@ export default function SQLInlineEditor({
|
|||
...tooltipExt,
|
||||
createStyleTheme(),
|
||||
...(allowMultiline ? [EditorView.lineWrapping] : []),
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
compartmentRef.current.of(
|
||||
sql({
|
||||
upperCaseKeywords: true,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,16 @@ export type SelectControlledProps = SelectProps &
|
|||
};
|
||||
|
||||
export default function SelectControlled(props: SelectControlledProps) {
|
||||
const { field, fieldState } = useController(props);
|
||||
const {
|
||||
field: {
|
||||
value: fieldValue,
|
||||
onChange: fieldOnChange,
|
||||
onBlur: fieldOnBlur,
|
||||
name: fieldName,
|
||||
ref: fieldRef,
|
||||
},
|
||||
fieldState,
|
||||
} = useController(props);
|
||||
const { onCreate, allowDeselect = true, ...restProps } = props;
|
||||
|
||||
// This is needed as mantine does not clear the select
|
||||
|
|
@ -17,9 +26,9 @@ export default function SelectControlled(props: SelectControlledProps) {
|
|||
// if it was previously in the data (ex. data was deleted)
|
||||
const selected = props.data?.find(d =>
|
||||
typeof d === 'string'
|
||||
? d === field.value
|
||||
? d === fieldValue
|
||||
: 'value' in d
|
||||
? d.value === field.value
|
||||
? d.value === fieldValue
|
||||
: true,
|
||||
);
|
||||
|
||||
|
|
@ -28,21 +37,21 @@ export default function SelectControlled(props: SelectControlledProps) {
|
|||
if (value === '_create_new_value' && onCreate != null) {
|
||||
onCreate();
|
||||
} else if (value !== null || allowDeselect) {
|
||||
field.onChange(value);
|
||||
fieldOnChange(value);
|
||||
}
|
||||
},
|
||||
[field, onCreate, allowDeselect],
|
||||
[fieldOnChange, onCreate, allowDeselect],
|
||||
);
|
||||
|
||||
return (
|
||||
<Select
|
||||
{...restProps}
|
||||
error={fieldState.error?.message}
|
||||
value={selected == null ? null : field.value}
|
||||
value={selected == null ? null : fieldValue}
|
||||
onChange={onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
onBlur={fieldOnBlur}
|
||||
name={fieldName}
|
||||
ref={fieldRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ describe('DBNumberChart', () => {
|
|||
const [numberFormat, setNumberFormat] = React.useState<
|
||||
NumberFormat | undefined
|
||||
>(undefined);
|
||||
// eslint-disable-next-line react-hooks/globals
|
||||
setNumberFormatFn = setNumberFormat;
|
||||
return <DBNumberChart config={{ ...baseTestConfig, numberFormat }} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ export function useAutoCompleteOptions(
|
|||
}, [formatter, fields, additionalSuggestions]);
|
||||
|
||||
// searchField is used for the purpose of checking if a key is valid and key values should be fetched
|
||||
// TODO: Come back and refactor how this works - it's not great and wouldn't catch a person copy-pasting some text
|
||||
const [searchField, setSearchField] = useState<Field | null>(null);
|
||||
// check if any search field matches
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ function useResizable(
|
|||
|
||||
const endResize = useCallback(() => {
|
||||
document.removeEventListener('mousemove', handleResize);
|
||||
// eslint-disable-next-line react-hooks/immutability
|
||||
document.removeEventListener('mouseup', endResize);
|
||||
}, [handleResize]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
/// Interface for all suggestion engines
|
||||
interface ISuggestionEngine {
|
||||
|
|
@ -82,12 +82,9 @@ export function useSqlSuggestions({
|
|||
input: string;
|
||||
enabled: boolean;
|
||||
}): Suggestion[] | null {
|
||||
const [suggestions, setSuggestions] = useState<Suggestion[] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
return useMemo(() => {
|
||||
if (!enabled) {
|
||||
setSuggestions(null);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
const suggestions: Suggestion[] = [];
|
||||
|
|
@ -99,8 +96,6 @@ export function useSqlSuggestions({
|
|||
});
|
||||
}
|
||||
}
|
||||
setSuggestions(suggestions.length > 0 ? suggestions : null);
|
||||
return suggestions.length > 0 ? suggestions : null;
|
||||
}, [input, enabled]);
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@ import { useWordmark } from './theme/ThemeProvider';
|
|||
import useNextraSeoProps from './useNextraSeoProps';
|
||||
|
||||
function ThemedLogo() {
|
||||
const Wordmark = useWordmark();
|
||||
return <Wordmark />;
|
||||
return useWordmark();
|
||||
}
|
||||
|
||||
const theme = {
|
||||
|
|
|
|||
|
|
@ -217,14 +217,21 @@ export function useAppTheme(): ThemeContextValue {
|
|||
}
|
||||
|
||||
// Convenience hooks
|
||||
// NOTE: These hooks return JSX elements, not component references, to avoid
|
||||
// creating components during render (which would violate react-hooks/static-components)
|
||||
export function useWordmark() {
|
||||
const { theme } = useAppTheme();
|
||||
return theme.Wordmark;
|
||||
const WordmarkComponent = theme.Wordmark;
|
||||
return useMemo(() => <WordmarkComponent />, [WordmarkComponent]);
|
||||
}
|
||||
|
||||
export function useLogomark() {
|
||||
export function useLogomark(props?: { size?: number }) {
|
||||
const { theme } = useAppTheme();
|
||||
return theme.Logomark;
|
||||
const LogomarkComponent = theme.Logomark;
|
||||
return useMemo(
|
||||
() => <LogomarkComponent {...props} />,
|
||||
[LogomarkComponent, props],
|
||||
);
|
||||
}
|
||||
|
||||
// Hook to get current theme name (useful for conditional rendering)
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ describe('ThemeProvider', () => {
|
|||
const { result } = renderHook(() => useWordmark(), { wrapper });
|
||||
|
||||
expect(result.current).toBeDefined();
|
||||
expect(typeof result.current).toBe('function');
|
||||
expect(typeof result.current).toBe('object');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -140,7 +140,7 @@ describe('ThemeProvider', () => {
|
|||
const { result } = renderHook(() => useLogomark(), { wrapper });
|
||||
|
||||
expect(result.current).toBeDefined();
|
||||
expect(typeof result.current).toBe('function');
|
||||
expect(typeof result.current).toBe('object');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -242,11 +242,13 @@ export function useTimeQuery({
|
|||
liveTailTimeRange == null &&
|
||||
tempLiveTailTimeRange == null &&
|
||||
!isInputTimeQueryLive(inputTimeQuery) &&
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
inputTimeQueryDerivedTimeQueryRef.current != null
|
||||
) {
|
||||
// Use the input time query, allows users to specify relative time ranges
|
||||
// via url ex. /logs?tq=Last+30+minutes
|
||||
// return inputTimeQueryDerivedTimeQuery as [Date, Date];
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
return inputTimeQueryDerivedTimeQueryRef.current;
|
||||
} else if (
|
||||
isReady &&
|
||||
|
|
@ -339,6 +341,7 @@ export function useTimeQuery({
|
|||
],
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
return {
|
||||
isReady, // Don't search until we know what we want to do
|
||||
isLive,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export const QueryParamProvider = ({
|
|||
|
||||
const setState = useCallback(
|
||||
(state: Record<string, any>) => {
|
||||
// eslint-disable-next-line react-hooks/immutability
|
||||
setCache(oldCache => {
|
||||
const newCache = {
|
||||
...oldCache,
|
||||
|
|
|
|||
|
|
@ -689,6 +689,7 @@ export const usePrevious = <T>(value: T): T | undefined => {
|
|||
useEffect(() => {
|
||||
ref.current = value;
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
return ref.current;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ const USE_FULLSTACK = process.env.E2E_FULLSTACK === 'true';
|
|||
|
||||
// Extend the base test to automatically handle Tanstack devtools
|
||||
export const test = base.extend({
|
||||
page: async ({ page }, use) => {
|
||||
page: async ({ page }, fn) => {
|
||||
// Note: page.addInitScript runs in the browser context, which cannot access Node.js
|
||||
// environment variables directly. We pass USE_FULLSTACK as a parameter so the browser
|
||||
// script can determine whether to set up demo connections (local mode) or rely on
|
||||
|
|
@ -153,7 +153,7 @@ export const test = base.extend({
|
|||
);
|
||||
}
|
||||
}, USE_FULLSTACK);
|
||||
await use(page);
|
||||
await fn(page);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -4379,6 +4379,7 @@ __metadata:
|
|||
eslint-config-next: "npm:^16.0.10"
|
||||
eslint-plugin-playwright: "npm:^2.4.0"
|
||||
eslint-plugin-react-hook-form: "npm:^0.3.1"
|
||||
eslint-plugin-react-hooks: "npm:^7.0.1"
|
||||
eslint-plugin-storybook: "npm:10.1.4"
|
||||
flat: "npm:^5.0.2"
|
||||
fuse.js: "npm:^6.6.2"
|
||||
|
|
|
|||
Loading…
Reference in a new issue