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

View File

@ -16,12 +16,10 @@ import { useDebounceFn, useMemoizedFn } from '@/hooks';
import { prepareMetricUpdateMetric, resolveEmptyMetric } from '@/lib/metrics'; import { prepareMetricUpdateMetric, resolveEmptyMetric } from '@/lib/metrics';
import { create } from 'mutative'; import { create } from 'mutative';
import { ShareRole } from '@/api/asset_interfaces/share'; import { ShareRole } from '@/api/asset_interfaces/share';
import { useUpdateMetric } from '@/api/buster_rest/metrics';
const useBusterMetrics = () => { const useBusterMetrics = () => {
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const { metricId: selectedMetricId } = useParams<{ metricId: string }>(); const { metricId: selectedMetricId } = useParams<{ metricId: string }>();
const { mutateAsync: updateMetricMutation } = useUpdateMetric();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const getMetricId = useMemoizedFn((metricId?: string): string => { 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'; '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 { interface DebounceOptions {
wait?: number; wait?: number;
@ -8,79 +11,38 @@ interface DebounceOptions {
leading?: boolean; leading?: boolean;
} }
export function useDebounceFn<T extends (...args: any[]) => any>( type noop = (...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);
// Update the function ref when fn changes export function useDebounceFn<T extends noop>(fn: T, options?: DebounceOptions) {
useEffect(() => { const fnRef = useLatest(fn);
fnRef.current = fn;
}, [fn]);
const cancel = useCallback(() => { const wait = options?.wait ?? 1000;
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
if (maxTimeoutRef.current) {
clearTimeout(maxTimeoutRef.current);
}
}, []);
const run = useCallback( const debounced = useMemo(
(...args: Parameters<T>) => { () =>
argsRef.current = args; debounce(
const now = Date.now(); (...args: Parameters<T>): ReturnType<T> => {
return fnRef.current(...args);
if (leading && !timeoutRef.current) { },
fnRef.current(...args); wait,
lastCallTime.current = now; options
} ),
[]
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);
}
},
[wait, maxWait, leading, cancel]
); );
// Clean up timeouts on unmount useUnmount(() => {
useEffect(() => { debounced.cancel();
return () => cancel(); });
}, [cancel]);
return { return {
run, run: debounced,
cancel cancel: debounced.cancel,
flush: debounced.flush
}; };
} }
export default useDebounceFn;
export function useDebounce<T>(value: T, options: DebounceOptions = {}) { export function useDebounce<T>(value: T, options: DebounceOptions = {}) {
const { wait = 1000, maxWait } = options; const { wait = 1000, maxWait } = options;
const [debouncedValue, setDebouncedValue] = useState<T>(value); 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;