mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
chore: add lint rules to treat missing hook dependencies as errors (#1420)
Tested on each of the spots that had hooks that were changed, seems good
This commit is contained in:
parent
991bd7e615
commit
815e6424a0
25 changed files with 154 additions and 105 deletions
5
.changeset/sweet-houses-scream.md
Normal file
5
.changeset/sweet-houses-scream.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@hyperdx/app": patch
|
||||
---
|
||||
|
||||
chore: treat missing react hook dependencies as errors
|
||||
|
|
@ -1,7 +1,12 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['simple-import-sort', '@typescript-eslint', 'prettier'],
|
||||
plugins: [
|
||||
'simple-import-sort',
|
||||
'@typescript-eslint',
|
||||
'prettier',
|
||||
'react-hooks',
|
||||
],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.json'],
|
||||
|
|
@ -23,6 +28,7 @@ module.exports = {
|
|||
'react/display-name': 'off',
|
||||
'simple-import-sort/exports': 'error',
|
||||
'simple-import-sort/imports': 'error',
|
||||
'react-hooks/exhaustive-deps': 'error',
|
||||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||||
},
|
||||
overrides: [
|
||||
|
|
|
|||
|
|
@ -363,7 +363,7 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
|
|||
isLoading: isLogViewsLoading,
|
||||
refetch: refetchLogViews,
|
||||
} = useSavedSearches();
|
||||
const logViews = logViewsData ?? [];
|
||||
const logViews = useMemo(() => logViewsData ?? [], [logViewsData]);
|
||||
|
||||
const updateDashboard = useUpdateDashboard();
|
||||
const updateLogView = useUpdateSavedSearch();
|
||||
|
|
@ -373,7 +373,7 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
|
|||
isLoading: isDashboardsLoading,
|
||||
refetch: refetchDashboards,
|
||||
} = useDashboards();
|
||||
const dashboards = dashboardsData ?? [];
|
||||
const dashboards = useMemo(() => dashboardsData ?? [], [dashboardsData]);
|
||||
|
||||
const router = useRouter();
|
||||
const { pathname, query } = router;
|
||||
|
|
|
|||
|
|
@ -176,13 +176,13 @@ function BenchmarkPage() {
|
|||
) {
|
||||
return;
|
||||
}
|
||||
setQueries(data.queries);
|
||||
setConnections(data.connections);
|
||||
setQueries(data.queries || []);
|
||||
setConnections(data.connections || []);
|
||||
setIterations(data.iterations);
|
||||
};
|
||||
|
||||
const _queries = queries || [];
|
||||
const _connections = connections || [];
|
||||
const _queries = useMemo(() => queries || [], [queries]);
|
||||
const _connections = useMemo(() => connections || [], [connections]);
|
||||
|
||||
const { data: estimates } = useEstimates(
|
||||
{ queries: _queries, connections: _connections },
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ const Tile = forwardRef(
|
|||
.getElementById(`chart-${chart.id}`)
|
||||
?.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, []);
|
||||
}, [chart.id, isHighlighed]);
|
||||
|
||||
const [queriedConfig, setQueriedConfig] = useState<
|
||||
ChartConfigWithDateRange | undefined
|
||||
|
|
@ -776,6 +776,7 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
|
|||
where,
|
||||
whereLanguage,
|
||||
onTimeRangeSelect,
|
||||
filterQueries,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -530,6 +530,7 @@ function useSearchedConfigToChartConfig({
|
|||
where,
|
||||
whereLanguage,
|
||||
defaultOrderBy,
|
||||
orderBy,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -1072,7 +1073,7 @@ function DBSearchPage() {
|
|||
...chartConfig,
|
||||
dateRange: searchedTimeRange,
|
||||
};
|
||||
}, [me?.team, chartConfig, searchedTimeRange]);
|
||||
}, [chartConfig, searchedTimeRange]);
|
||||
|
||||
const displayedColumns = splitAndTrimWithBracket(
|
||||
dbSqlRowTableConfig?.select ??
|
||||
|
|
@ -1243,7 +1244,7 @@ function DBSearchPage() {
|
|||
onTimeRangeSelect(d1, d2);
|
||||
setIsLive(false);
|
||||
},
|
||||
[onTimeRangeSelect],
|
||||
[onTimeRangeSelect, setIsLive],
|
||||
);
|
||||
|
||||
const filtersChartConfig = useMemo<ChartConfigWithDateRange>(() => {
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@ export default function DOMPlayer({
|
|||
}, [setPlayerTime]);
|
||||
|
||||
const updatePlayerTimeRafRef = useRef(0);
|
||||
const updatePlayerTime = () => {
|
||||
const updatePlayerTime = useCallback(() => {
|
||||
if (
|
||||
replayer.current != null &&
|
||||
replayer.current.service.state.matches('playing')
|
||||
|
|
@ -257,7 +257,7 @@ export default function DOMPlayer({
|
|||
}
|
||||
|
||||
updatePlayerTimeRafRef.current = requestAnimationFrame(updatePlayerTime);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Update timestamp ui in timeline
|
||||
useEffect(() => {
|
||||
|
|
@ -265,7 +265,7 @@ export default function DOMPlayer({
|
|||
return () => {
|
||||
cancelAnimationFrame(updatePlayerTimeRafRef.current);
|
||||
};
|
||||
}, []);
|
||||
}, [updatePlayerTime]);
|
||||
|
||||
// Manage playback pause/play state, rrweb only
|
||||
useEffect(() => {
|
||||
|
|
@ -398,6 +398,7 @@ export default function DOMPlayer({
|
|||
isInitialEventsLoaded,
|
||||
playerState,
|
||||
play,
|
||||
debug,
|
||||
]);
|
||||
|
||||
// Set player to the correct time based on focus
|
||||
|
|
@ -440,7 +441,7 @@ export default function DOMPlayer({
|
|||
}
|
||||
abort();
|
||||
};
|
||||
}, []);
|
||||
}, [abort]);
|
||||
|
||||
const isLoading = isInitialEventsLoaded === false && isSearchResultsFetching;
|
||||
// TODO: Handle when ts is set to a value that's outside of this session
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export default function Playbar({
|
|||
queryKey: ['PlayBar', queriedConfig],
|
||||
},
|
||||
);
|
||||
const events: any[] = data?.data ?? [];
|
||||
const events: any[] = useMemo(() => data?.data ?? [], [data?.data]);
|
||||
|
||||
const markers = useMemo<PlaybarMarker[]>(() => {
|
||||
return uniqBy(
|
||||
|
|
|
|||
|
|
@ -320,10 +320,10 @@ export default function PodDetailsSidePanel({
|
|||
}
|
||||
return _where;
|
||||
}, [
|
||||
nodeName,
|
||||
logSource,
|
||||
doesPrimaryOrSortingKeysContainServiceExpression,
|
||||
logServiceNames,
|
||||
podName,
|
||||
]);
|
||||
|
||||
const handleClose = React.useCallback(() => {
|
||||
|
|
|
|||
|
|
@ -906,7 +906,13 @@ function ServicesDashboardPage() {
|
|||
) {
|
||||
onSubmit();
|
||||
}
|
||||
}, [service, sourceId]);
|
||||
}, [
|
||||
service,
|
||||
sourceId,
|
||||
appliedConfig.service,
|
||||
appliedConfig.source,
|
||||
onSubmit,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Box p="sm">
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ export const SessionEventList = ({
|
|||
});
|
||||
const formatTime = useFormatTime();
|
||||
|
||||
const events = data?.data ?? [];
|
||||
const events = React.useMemo(() => data?.data ?? [], [data?.data]);
|
||||
|
||||
const getRowWhere = useRowWhere({ meta: data?.meta, aliasMap });
|
||||
|
||||
|
|
|
|||
|
|
@ -306,7 +306,7 @@ export default function SessionsPage() {
|
|||
if (sourceId !== appliedConfig.sessionSource) {
|
||||
onSubmit();
|
||||
}
|
||||
}, [sourceId]);
|
||||
}, [sourceId, appliedConfig.sessionSource, onSubmit]);
|
||||
|
||||
// FIXME: fix the url
|
||||
const generateSearchUrl = useCallback(
|
||||
|
|
|
|||
|
|
@ -840,7 +840,7 @@ function TeamNameSection() {
|
|||
},
|
||||
);
|
||||
},
|
||||
[refetchTeam, setTeamName, team?.name],
|
||||
[refetchTeam, setTeamName],
|
||||
);
|
||||
return (
|
||||
<Box id="team_name">
|
||||
|
|
|
|||
|
|
@ -410,16 +410,20 @@ export default function TimelineChart({
|
|||
}
|
||||
};
|
||||
|
||||
useDrag(timelineRef, [], {
|
||||
onDrag: e => {
|
||||
setOffset(v =>
|
||||
Math.min(
|
||||
Math.max(v - e.movementX * (0.125 / scale), 0),
|
||||
100 - 100 / scale,
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
const useDragOptions: Parameters<typeof useDrag>[1] = useMemo(
|
||||
() => ({
|
||||
onDrag: e => {
|
||||
setOffset(v =>
|
||||
Math.min(
|
||||
Math.max(v - e.movementX * (0.125 / scale), 0),
|
||||
100 - 100 / scale,
|
||||
),
|
||||
);
|
||||
},
|
||||
}),
|
||||
[scale, setOffset],
|
||||
);
|
||||
useDrag(timelineRef, useDragOptions);
|
||||
|
||||
const [cursorXPerc, setCursorXPerc] = useState(0);
|
||||
|
||||
|
|
|
|||
|
|
@ -239,6 +239,10 @@ export default function ContextSubpanel({
|
|||
originalLanguage,
|
||||
newDateRange,
|
||||
contextBy,
|
||||
source.connection,
|
||||
source.defaultTableSelectExpression,
|
||||
source.from,
|
||||
source.timestampValueExpression,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -477,7 +477,7 @@ export default function EditTimeChartForm({
|
|||
if (displayType !== DisplayType.Line) {
|
||||
setValue('alert', undefined);
|
||||
}
|
||||
}, [displayType]);
|
||||
}, [displayType, setValue]);
|
||||
|
||||
const showGeneratedSql = ['table', 'time', 'number'].includes(activeTab); // Whether to show the generated SQL preview
|
||||
const showSampleEvents = tableSource?.kind !== SourceKind.Metric;
|
||||
|
|
|
|||
|
|
@ -90,12 +90,15 @@ export function RowOverviewPanel({
|
|||
const flattenedEventAttributes =
|
||||
firstRow?.__hdx_event_attributes ?? EMPTY_OBJ;
|
||||
|
||||
const dataAttributes =
|
||||
eventAttributesExpr &&
|
||||
firstRow?.[eventAttributesExpr] &&
|
||||
Object.keys(firstRow[eventAttributesExpr]).length > 0
|
||||
? { [eventAttributesExpr]: firstRow[eventAttributesExpr] }
|
||||
: {};
|
||||
const dataAttributes = useMemo(
|
||||
() =>
|
||||
eventAttributesExpr &&
|
||||
firstRow?.[eventAttributesExpr] &&
|
||||
Object.keys(firstRow[eventAttributesExpr]).length > 0
|
||||
? { [eventAttributesExpr]: firstRow[eventAttributesExpr] }
|
||||
: {},
|
||||
[eventAttributesExpr, firstRow],
|
||||
);
|
||||
|
||||
const _generateSearchUrl = useCallback(
|
||||
(query?: string, queryLanguage?: 'sql' | 'lucene') => {
|
||||
|
|
|
|||
|
|
@ -151,27 +151,28 @@ export default function DBRowSidePanelHeader({
|
|||
[bodyExpanded, mainContent],
|
||||
);
|
||||
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
const [headerElement, setHeaderElement] = useState<HTMLDivElement | null>(
|
||||
null,
|
||||
);
|
||||
const [headerHeight, setHeaderHeight] = useState(0);
|
||||
useEffect(() => {
|
||||
if (!headerRef.current) return;
|
||||
const el = headerRef.current;
|
||||
if (!headerElement) return;
|
||||
|
||||
const updateHeight = () => {
|
||||
const newHeight = el.offsetHeight;
|
||||
const newHeight = headerElement.offsetHeight;
|
||||
setHeaderHeight(newHeight);
|
||||
};
|
||||
updateHeight();
|
||||
|
||||
// Set up a resize observer to detect height changes
|
||||
const resizeObserver = new ResizeObserver(updateHeight);
|
||||
resizeObserver.observe(el);
|
||||
resizeObserver.observe(headerElement);
|
||||
|
||||
// Clean up the observer on component unmount
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [headerRef.current, setHeaderHeight]);
|
||||
}, [headerElement]);
|
||||
|
||||
const { userPreferences, setUserPreference } = useUserPreferences();
|
||||
const { expandSidebarHeader } = userPreferences;
|
||||
|
|
@ -221,7 +222,7 @@ export default function DBRowSidePanelHeader({
|
|||
overflow: 'auto',
|
||||
overflowWrap: 'break-word',
|
||||
}}
|
||||
ref={headerRef}
|
||||
ref={setHeaderElement}
|
||||
>
|
||||
<Flex justify="space-between" mb="xs">
|
||||
<Text size="xs">{mainContentHeader}</Text>
|
||||
|
|
|
|||
|
|
@ -531,6 +531,7 @@ export const RawLogTable = memo(
|
|||
expandedRows,
|
||||
toggleRowExpansion,
|
||||
showExpandButton,
|
||||
aliasMap,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -288,7 +288,7 @@ export function useEventsAroundFocus({
|
|||
id: rowWhere(omit(cd, ['SpanAttributes', '__hdx_hidden'])),
|
||||
};
|
||||
});
|
||||
}, [afterSpanData, beforeSpanData]);
|
||||
}, [afterSpanData, beforeSpanData, meta, rowWhere, type]);
|
||||
|
||||
return {
|
||||
rows,
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ export function MetricNameSelect({
|
|||
});
|
||||
}
|
||||
return metricsFromQuery;
|
||||
}, [gaugeMetrics, histogramMetrics, sumMetrics]);
|
||||
}, [gaugeMetrics, histogramMetrics, sumMetrics, metricName, metricType]);
|
||||
|
||||
return (
|
||||
<Select
|
||||
|
|
|
|||
|
|
@ -225,23 +225,21 @@ export default function SQLInlineEditor({
|
|||
// query search history
|
||||
const [queryHistory, setQueryHistory] = useQueryHistory(queryHistoryType);
|
||||
|
||||
const onSelectSearchHistory = (
|
||||
view: EditorView,
|
||||
from: number,
|
||||
to: number,
|
||||
q: string,
|
||||
) => {
|
||||
// update history into search bar
|
||||
view.dispatch({
|
||||
changes: { from, to, insert: q },
|
||||
});
|
||||
// close history bar;
|
||||
closeCompletion(view);
|
||||
// update history order
|
||||
setQueryHistory(q);
|
||||
// execute search
|
||||
if (onSubmit) onSubmit();
|
||||
};
|
||||
const onSelectSearchHistory = useCallback(
|
||||
(view: EditorView, from: number, to: number, q: string) => {
|
||||
// update history into search bar
|
||||
view.dispatch({
|
||||
changes: { from, to, insert: q },
|
||||
});
|
||||
// close history bar;
|
||||
closeCompletion(view);
|
||||
// update history order
|
||||
setQueryHistory(q);
|
||||
// execute search
|
||||
if (onSubmit) onSubmit();
|
||||
},
|
||||
[onSubmit, setQueryHistory],
|
||||
);
|
||||
|
||||
const createHistoryList = useMemo(() => {
|
||||
return () => {
|
||||
|
|
@ -264,7 +262,7 @@ export default function SQLInlineEditor({
|
|||
}),
|
||||
};
|
||||
};
|
||||
}, [queryHistory]);
|
||||
}, [queryHistory, onSelectSearchHistory]);
|
||||
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
|
|
@ -304,7 +302,12 @@ export default function SQLInlineEditor({
|
|||
),
|
||||
});
|
||||
},
|
||||
[filteredFields, additionalSuggestions, queryHistory],
|
||||
[
|
||||
filteredFields,
|
||||
additionalSuggestions,
|
||||
createHistoryList,
|
||||
disableKeywordAutocomplete,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -1133,29 +1133,32 @@ export function TableSourceForm({
|
|||
}, [watch, kind, currentSourceId, sources, updateSource]);
|
||||
|
||||
const sourceFormSchema = sourceSchemaWithout({ id: true });
|
||||
const handleError = (error: z.ZodError<TSourceUnion>) => {
|
||||
const errors = error.errors;
|
||||
for (const err of errors) {
|
||||
const errorPath: string = err.path.join('.');
|
||||
// TODO: HDX-1768 get rid of this type assertion if possible
|
||||
setError(errorPath as any, { ...err });
|
||||
}
|
||||
notifications.show({
|
||||
color: 'red',
|
||||
message: (
|
||||
<Stack>
|
||||
<Text size="sm">
|
||||
<b>Failed to create source</b>
|
||||
</Text>
|
||||
{errors.map((err, i) => (
|
||||
<Text key={i} size="sm">
|
||||
✖ {err.message}
|
||||
const handleError = useCallback(
|
||||
(error: z.ZodError<TSourceUnion>) => {
|
||||
const errors = error.errors;
|
||||
for (const err of errors) {
|
||||
const errorPath: string = err.path.join('.');
|
||||
// TODO: HDX-1768 get rid of this type assertion if possible
|
||||
setError(errorPath as any, { ...err });
|
||||
}
|
||||
notifications.show({
|
||||
color: 'red',
|
||||
message: (
|
||||
<Stack>
|
||||
<Text size="sm">
|
||||
<b>Failed to create source</b>
|
||||
</Text>
|
||||
))}
|
||||
</Stack>
|
||||
),
|
||||
});
|
||||
};
|
||||
{errors.map((err, i) => (
|
||||
<Text key={i} size="sm">
|
||||
✖ {err.message}
|
||||
</Text>
|
||||
))}
|
||||
</Stack>
|
||||
),
|
||||
});
|
||||
},
|
||||
[setError],
|
||||
);
|
||||
|
||||
const _onCreate = useCallback(() => {
|
||||
clearErrors();
|
||||
|
|
@ -1215,11 +1218,12 @@ export function TableSourceForm({
|
|||
);
|
||||
})();
|
||||
}, [
|
||||
clearErrors,
|
||||
handleError,
|
||||
sourceFormSchema,
|
||||
handleSubmit,
|
||||
createSource,
|
||||
onCreate,
|
||||
kind,
|
||||
formState,
|
||||
sources,
|
||||
updateSource,
|
||||
]);
|
||||
|
|
@ -1252,7 +1256,14 @@ export function TableSourceForm({
|
|||
},
|
||||
);
|
||||
})();
|
||||
}, [handleSubmit, updateSource, onSave]);
|
||||
}, [
|
||||
handleSubmit,
|
||||
updateSource,
|
||||
onSave,
|
||||
clearErrors,
|
||||
handleError,
|
||||
sourceFormSchema,
|
||||
]);
|
||||
|
||||
const databaseName = watch(`from.databaseName`, DEFAULT_DATABASE);
|
||||
const connectionId = watch(`connection`);
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ export function useAutoCompleteOptions(
|
|||
});
|
||||
});
|
||||
return output;
|
||||
}, [fieldCompleteOptions, keyVals, searchField]);
|
||||
}, [fieldCompleteOptions, keyVals, searchField, formatter]);
|
||||
|
||||
// combine all autocomplete options
|
||||
return useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|||
window.removeEventListener('customStorage', handleCustomStorageChange);
|
||||
window.removeEventListener('storage', handleStorageChange);
|
||||
};
|
||||
}, []);
|
||||
}, [instanceId, key]);
|
||||
|
||||
// Fetch the value on client-side to avoid SSR issues
|
||||
useEffect(() => {
|
||||
|
|
@ -314,15 +314,18 @@ export function useQueryHistory<T>(type: string | undefined) {
|
|||
|
||||
export function useIntersectionObserver(onIntersect: () => void) {
|
||||
const observer = useRef<IntersectionObserver | null>(null);
|
||||
const observerRef = useCallback((node: Element | null) => {
|
||||
if (observer.current) observer.current.disconnect();
|
||||
observer.current = new IntersectionObserver(entries => {
|
||||
if (entries[0].isIntersecting) {
|
||||
onIntersect();
|
||||
}
|
||||
});
|
||||
if (node) observer.current.observe(node);
|
||||
}, []);
|
||||
const observerRef = useCallback(
|
||||
(node: Element | null) => {
|
||||
if (observer.current) observer.current.disconnect();
|
||||
observer.current = new IntersectionObserver(entries => {
|
||||
if (entries[0].isIntersecting) {
|
||||
onIntersect();
|
||||
}
|
||||
});
|
||||
if (node) observer.current.observe(node);
|
||||
},
|
||||
[onIntersect],
|
||||
);
|
||||
|
||||
return { observerRef };
|
||||
}
|
||||
|
|
@ -509,7 +512,6 @@ export const usePrevious = <T>(value: T): T | undefined => {
|
|||
// From https://javascript.plainenglish.io/how-to-make-a-simple-custom-usedrag-react-hook-6b606d45d353
|
||||
export const useDrag = (
|
||||
ref: MutableRefObject<HTMLDivElement | null>,
|
||||
deps = [],
|
||||
options: {
|
||||
onDrag?: (e: PointerEvent) => any;
|
||||
onPointerDown?: (e: PointerEvent) => any;
|
||||
|
|
@ -559,9 +561,9 @@ export const useDrag = (
|
|||
element.removeEventListener('pointermove', handlePointerMove);
|
||||
};
|
||||
}
|
||||
|
||||
return () => {};
|
||||
}, [...deps, isDragging]);
|
||||
// disable dependency array as this doesn't fit nicely with react
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return { isDragging };
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue