create a hook for update charts

This commit is contained in:
Nate Kelley 2025-03-13 10:46:03 -06:00
parent 77ece34d25
commit ae38da2de1
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
5 changed files with 182 additions and 76 deletions

View File

@ -1,6 +1,6 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { QueryClient } from '@tanstack/react-query';
import { useMemoizedFn } from '@/hooks';
import { useMemoizedFn, useDebounceFn } from '@/hooks';
import {
deleteMetrics,
getMetric,
@ -13,7 +13,7 @@ import {
import type { GetMetricParams, ListMetricsParams, UpdateMetricParams } from './interfaces';
import { upgradeMetricToIMetric } from '@/lib/chat';
import { queryKeys } from '@/api/query_keys';
import { useMemo } from 'react';
import { useMemo, useTransition } from 'react';
import { useBusterAssetsContextSelector } from '@/context/Assets/BusterAssetsProvider';
import { resolveEmptyMetric } from '@/lib/metrics/resolve';
import { useGetUserFavorites } from '../users';
@ -150,10 +150,13 @@ export const useSaveMetric = () => {
* It will also strip out any values that are not changed from the DEFAULT_CHART_CONFIG.
* It will also update the draft_session_id if it exists.
*/
export const useUpdateMetric = () => {
export const useUpdateMetric = (params?: { wait?: number }) => {
const [isPending, startTransition] = useTransition();
const queryClient = useQueryClient();
const { mutateAsync: saveMetric } = useSaveMetric();
const mutationFn = useMemoizedFn(
const waitTime = params?.wait || 0;
const combineAndSaveMetric = useMemoizedFn(
async (newMetricPartial: Partial<IBusterMetric> & { id: string }) => {
const metricId = newMetricPartial.id;
const options = queryKeys.metricsGetMetric(metricId);
@ -163,17 +166,41 @@ export const useUpdateMetric = () => {
});
if (prevMetric && newMetric) {
queryClient.setQueryData(options.queryKey, newMetric);
}
return { newMetric, prevMetric };
}
);
const mutationFn = useMemoizedFn(
async (newMetricPartial: Partial<IBusterMetric> & { id: string }) => {
const { newMetric, prevMetric } = await combineAndSaveMetric(newMetricPartial);
if (newMetric && prevMetric) {
startTransition(() => {
const changedValues = prepareMetricUpdateMetric(newMetric, prevMetric);
if (changedValues) {
return saveMetric(changedValues);
saveMetric(changedValues);
}
});
}
return Promise.resolve(newMetric!);
}
);
return useMutation({
mutationFn
const mutationRes = useMutation({
mutationFn: mutationFn
});
const { run: mutateDebounced } = useDebounceFn(mutationRes.mutateAsync, { wait: waitTime });
return useMemo(
() => ({
...mutationRes,
mutateDebounced
}),
[mutationRes, mutateDebounced]
);
};
export const useDeleteMetric = () => {

View File

@ -16,12 +16,10 @@ import { useDebounceFn, useMemoizedFn } from '@/hooks';
import { prepareMetricUpdateMetric, resolveEmptyMetric } from '@/lib/metrics';
import { create } from 'mutative';
import { ShareRole } from '@/api/asset_interfaces/share';
import { useUpdateMetric } from '@/api/buster_rest/metrics';
const useBusterMetrics = () => {
const [isPending, startTransition] = useTransition();
const { metricId: selectedMetricId } = useParams<{ metricId: string }>();
const { mutateAsync: updateMetricMutation } = useUpdateMetric();
const queryClient = useQueryClient();
const getMetricId = useMemoizedFn((metricId?: string): string => {

View File

@ -0,0 +1,107 @@
import {
ColumnSettings,
DEFAULT_CHART_CONFIG,
IColumnLabelFormat,
type IBusterMetric,
type IBusterMetricChartConfig
} from '@/api/asset_interfaces/metric';
import { useUpdateMetric } from '@/api/buster_rest/metrics';
import { queryKeys } from '@/api/query_keys';
import { useMemoizedFn } from '@/hooks';
import { resolveEmptyMetric } from '@/lib/metrics/resolve';
import { useQueryClient } from '@tanstack/react-query';
export const useUpdateMetricChart = ({ metricId }: { metricId: string }) => {
const queryClient = useQueryClient();
const { mutateDebounced: onUpdateMetricDebounced } = useUpdateMetric({ wait: 600 });
const getMetricMemoized = useMemoizedFn((metricIdProp?: string): IBusterMetric => {
const options = queryKeys.metricsGetMetric(metricIdProp || metricId);
const data = queryClient.getQueryData(options.queryKey);
return resolveEmptyMetric(data, metricIdProp || metricId);
});
const onUpdateMetricChartConfig = useMemoizedFn(
({
chartConfig,
ignoreUndoRedo
}: {
chartConfig: Partial<IBusterMetricChartConfig>;
ignoreUndoRedo?: boolean;
}) => {
const currentMetric = getMetricMemoized();
if (!ignoreUndoRedo) {
// undoRedoParams.addToUndoStack({
// metricId: editMetric.id,
// messageId: editMessage.id,
// chartConfig: editMessage.chart_config
// });
}
const newChartConfig: IBusterMetricChartConfig = {
...DEFAULT_CHART_CONFIG,
...currentMetric.chart_config,
...chartConfig
};
onUpdateMetricDebounced({
id: metricId,
chart_config: newChartConfig
});
}
);
const onUpdateColumnLabelFormat = useMemoizedFn(
({
columnId,
columnLabelFormat
}: {
columnId: string;
columnLabelFormat: Partial<IColumnLabelFormat>;
}) => {
const currentMetric = getMetricMemoized();
const existingColumnLabelFormats = currentMetric.chart_config.columnLabelFormats;
const existingColumnLabelFormat = existingColumnLabelFormats[columnId];
const newColumnLabelFormat = {
...existingColumnLabelFormat,
...columnLabelFormat
};
const columnLabelFormats = {
...existingColumnLabelFormats,
[columnId]: newColumnLabelFormat
};
onUpdateMetricChartConfig({
chartConfig: {
columnLabelFormats
}
});
}
);
const onUpdateColumnSetting = useMemoizedFn(
({ columnId, columnSetting }: { columnId: string; columnSetting: Partial<ColumnSettings> }) => {
const currentMetric = getMetricMemoized();
const existingColumnSettings = currentMetric.chart_config.columnSettings;
const existingColumnSetting = currentMetric.chart_config.columnSettings[columnId];
const newColumnSetting: Required<ColumnSettings> = {
...existingColumnSetting,
...columnSetting
};
const newColumnSettings: Record<string, Required<ColumnSettings>> = {
...existingColumnSettings,
[columnId]: newColumnSetting
};
onUpdateMetricChartConfig({
chartConfig: {
columnSettings: newColumnSettings
}
});
}
);
return {
onUpdateMetricChartConfig,
onUpdateColumnLabelFormat,
onUpdateColumnSetting
};
};

View File

@ -1,6 +1,9 @@
'use client';
import { useEffect, useState, useRef, useCallback } from 'react';
import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
import useLatest from './useLatest';
import debounce from 'lodash/debounce';
import { useUnmount } from './useUnmount';
interface DebounceOptions {
wait?: number;
@ -8,79 +11,38 @@ interface DebounceOptions {
leading?: boolean;
}
export function useDebounceFn<T extends (...args: any[]) => any>(
fn: T,
options: DebounceOptions = {}
) {
const { wait = 1000, maxWait, leading = false } = options;
const timeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const maxTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const fnRef = useRef<T>(fn);
const argsRef = useRef<Parameters<T> | null>(null);
const lastCallTime = useRef<number>(0);
type noop = (...args: any[]) => any;
// Update the function ref when fn changes
useEffect(() => {
fnRef.current = fn;
}, [fn]);
export function useDebounceFn<T extends noop>(fn: T, options?: DebounceOptions) {
const fnRef = useLatest(fn);
const cancel = useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
if (maxTimeoutRef.current) {
clearTimeout(maxTimeoutRef.current);
}
}, []);
const wait = options?.wait ?? 1000;
const run = useCallback(
(...args: Parameters<T>) => {
argsRef.current = args;
const now = Date.now();
if (leading && !timeoutRef.current) {
fnRef.current(...args);
lastCallTime.current = now;
}
cancel();
timeoutRef.current = setTimeout(() => {
if (!leading && argsRef.current) {
fnRef.current(...argsRef.current);
}
lastCallTime.current = now;
}, wait);
// Handle maxWait
if (maxWait && !maxTimeoutRef.current && !leading) {
const timeSinceLastCall = now - lastCallTime.current;
const maxWaitTimeRemaining = Math.max(0, maxWait - timeSinceLastCall);
maxTimeoutRef.current = setTimeout(() => {
if (timeoutRef.current && argsRef.current) {
clearTimeout(timeoutRef.current);
fnRef.current(...argsRef.current);
lastCallTime.current = Date.now();
}
maxTimeoutRef.current = undefined;
}, maxWaitTimeRemaining);
}
const debounced = useMemo(
() =>
debounce(
(...args: Parameters<T>): ReturnType<T> => {
return fnRef.current(...args);
},
[wait, maxWait, leading, cancel]
wait,
options
),
[]
);
// Clean up timeouts on unmount
useEffect(() => {
return () => cancel();
}, [cancel]);
useUnmount(() => {
debounced.cancel();
});
return {
run,
cancel
run: debounced,
cancel: debounced.cancel,
flush: debounced.flush
};
}
export default useDebounceFn;
export function useDebounce<T>(value: T, options: DebounceOptions = {}) {
const { wait = 1000, maxWait } = options;
const [debouncedValue, setDebouncedValue] = useState<T>(value);

View File

@ -0,0 +1,12 @@
'use client';
import { useRef } from 'react';
export function useLatest<T>(value: T) {
const ref = useRef(value);
ref.current = value;
return ref;
}
export default useLatest;