teat pot error update 🫖

This commit is contained in:
Nate Kelley 2025-04-10 16:05:25 -06:00
parent 4548b07cfe
commit 3da71f4c91
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
9 changed files with 241 additions and 253 deletions

View File

@ -1,32 +1,34 @@
import { AxiosError } from 'axios';
import isString from 'lodash/isString'; import isString from 'lodash/isString';
export const rustErrorHandler = (errors: any = {}): RustApiError => { export const rustErrorHandler = (errors: any = {}): RustApiError => {
const data = errors?.response?.data; const data = errors?.response?.data;
const status = errors?.status;
if (data && isString(data)) { if (data && isString(data)) {
return { message: String(data) }; return { message: String(data), status };
} }
if (data && data?.message) { if (data && data?.message) {
return { message: String(data.message) }; return { message: String(data.message), status };
} }
if (data && data?.detail) { if (data && data?.detail) {
if (typeof data.detail === 'string') { if (typeof data.detail === 'string') {
return { message: String(data.detail) }; return { message: String(data.detail), status };
} }
if (data.detail?.[0]) { if (data.detail?.[0]) {
return { message: String(data.detail[0].msg) }; return { message: String(data.detail[0].msg), status };
} }
return { message: String(data.detail) }; return { message: String(data.detail), status };
} }
if (errors?.message) { if (errors?.message) {
return { message: String(errors.message) }; return { message: String(errors.message), status };
} }
if (typeof errors === 'string') { if (typeof errors === 'string') {
return { message: String(errors) }; return { message: String(errors), status };
} }
return {}; return {};

View File

@ -21,7 +21,7 @@ import { collectionQueryKeys } from '@/api/query_keys/collection';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useBusterAssetsContextSelector } from '@/context/Assets/BusterAssetsProvider'; import { useBusterAssetsContextSelector } from '@/context/Assets/BusterAssetsProvider';
import { useGetUserFavorites } from '../users'; import { useGetUserFavorites } from '../users';
import type { IBusterMetric } from '@/api/asset_interfaces/metric'; import type { BusterMetricData, IBusterMetric } from '@/api/asset_interfaces/metric';
import { create } from 'mutative'; import { create } from 'mutative';
import { import {
useAddAssetToCollection, useAddAssetToCollection,
@ -81,7 +81,7 @@ export const useGetMetric = <TData = IBusterMetric>(
return useQuery({ return useQuery({
...options, ...options,
queryFn, queryFn,
enabled: !!id, enabled: false, //In the year of our lord 2025, April 10, I, Nate Kelley, decided to disable this query in favor of explicityly fetching the data. May god have mercy on our souls.
retry(failureCount, error) { retry(failureCount, error) {
if (error?.message !== undefined) { if (error?.message !== undefined) {
setAssetPasswordError(id!, error.message || 'An error occurred'); setAssetPasswordError(id!, error.message || 'An error occurred');
@ -112,13 +112,19 @@ export const useGetMetricsList = (
/** /**
* This is a hook that will use the version number from the URL params if it exists. * This is a hook that will use the version number from the URL params if it exists.
*/ */
export const useGetMetricData = ({ export const useGetMetricData = <TData = BusterMetricData>(
id, {
versionNumber: versionNumberProp id,
}: { versionNumber: versionNumberProp
id: string | undefined; }: {
versionNumber?: number; id: string | undefined;
}) => { versionNumber?: number;
},
params?: Omit<UseQueryOptions<BusterMetricData, RustApiError, TData>, 'queryKey' | 'queryFn'>
) => {
const getAssetPassword = useBusterAssetsContextSelector((x) => x.getAssetPassword);
const { password } = getAssetPassword(id!);
const { const {
isFetched: isFetchedMetric, isFetched: isFetchedMetric,
error: errorMetric, error: errorMetric,
@ -141,7 +147,7 @@ export const useGetMetricData = ({
}, [versionNumberProp, versionNumberFromParams]); }, [versionNumberProp, versionNumberFromParams]);
const queryFn = useMemoizedFn(() => { const queryFn = useMemoizedFn(() => {
return getMetricData({ id: id!, version_number: versionNumber }); return getMetricData({ id: id!, version_number: versionNumber, password });
}); });
return useQuery({ return useQuery({
@ -149,7 +155,9 @@ export const useGetMetricData = ({
queryFn, queryFn,
enabled: () => { enabled: () => {
return !!id && isFetchedMetric && !errorMetric && !!fetchedMetricData; return !!id && isFetchedMetric && !errorMetric && !!fetchedMetricData;
} },
select: params?.select,
...params
}); });
}; };

View File

@ -37,13 +37,15 @@ export const getMetric_server = async ({ id, password }: Parameters<typeof getMe
export const getMetricData = async ({ export const getMetricData = async ({
id, id,
version_number version_number,
password
}: { }: {
id: string; id: string;
version_number?: number; version_number?: number;
password?: string;
}) => { }) => {
return mainApi return mainApi
.get<BusterMetricData>(`/metrics/${id}/data`, { params: { version_number } }) .get<BusterMetricData>(`/metrics/${id}/data`, { params: { password, version_number } })
.then((res) => res.data); .then((res) => res.data);
}; };

View File

@ -166,7 +166,6 @@ export const ShareMenuContentPublish: React.FC<ShareMenuContentBodyProps> = Reac
<div className={cn('flex justify-end space-x-2 border-t', className)}> <div className={cn('flex justify-end space-x-2 border-t', className)}>
<Button <Button
block block
loading={isPublishing}
onClick={async (v) => { onClick={async (v) => {
onTogglePublish(false); onTogglePublish(false);
}}> }}>

View File

@ -1,29 +1,70 @@
'use client'; 'use client';
import type { ShareAssetType } from '@/api/asset_interfaces/share';
import { queryKeys } from '@/api/query_keys';
import { useMemoizedFn } from '@/hooks'; import { useMemoizedFn } from '@/hooks';
import { timeout } from '@/lib';
import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { createContext, useContextSelector } from 'use-context-selector'; import { createContext, useContextSelector } from 'use-context-selector';
const useBusterAssets = () => { const useBusterAssets = () => {
const [assetsToPasswords, setAssetsToPasswords] = useState<Record<string, string>>({}); const queryClient = useQueryClient();
const [assetsToPasswords, setAssetsToPasswords] = useState<
Record<
string,
{
password: string;
type: ShareAssetType;
}
>
>({});
const [assetsPasswordErrors, setAssetsPasswordErrors] = useState<Record<string, string | null>>( const [assetsPasswordErrors, setAssetsPasswordErrors] = useState<Record<string, string | null>>(
{} {}
); );
const setAssetPassword = useMemoizedFn((assetId: string, password: string) => { const invalidateAssetData = useMemoizedFn(async (assetId: string, type: ShareAssetType) => {
setAssetsToPasswords((prev) => ({ ...prev, [assetId]: password })); if (type === 'metric') {
removeAssetPasswordError(assetId); await queryClient.invalidateQueries({
queryKey: queryKeys.metricsGetMetric(assetId).queryKey
});
} else if (type === 'dashboard') {
await queryClient.invalidateQueries({
queryKey: queryKeys.dashboardGetDashboard(assetId).queryKey
});
} else if (type === 'collection') {
await queryClient.invalidateQueries({
queryKey: queryKeys.collectionsGetCollection(assetId).queryKey
});
} else if (type === 'chat') {
await queryClient.invalidateQueries({
queryKey: queryKeys.chatsGetChat(assetId).queryKey
});
} else {
const exhaustiveCheck: never = type;
}
}); });
const onSetAssetPassword = useMemoizedFn(
async (assetId: string, password: string, type: ShareAssetType) => {
setAssetsToPasswords((prev) => ({ ...prev, [assetId]: { password, type } }));
removeAssetPasswordError(assetId);
await timeout(150);
await invalidateAssetData(assetId, type);
}
);
const getAssetPassword = useCallback( const getAssetPassword = useCallback(
( (
assetId: string assetId: string
): { ): {
password: undefined | string; password: undefined | string;
error: string | null; error: string | null;
type: ShareAssetType | undefined;
} => { } => {
return { return {
password: assetsToPasswords[assetId] || undefined, password: assetsToPasswords[assetId]?.password || undefined,
type: assetsToPasswords[assetId]?.type || undefined,
error: assetsPasswordErrors[assetId] || null error: assetsPasswordErrors[assetId] || null
}; };
}, },
@ -43,7 +84,7 @@ const useBusterAssets = () => {
return { return {
setAssetPasswordError, setAssetPasswordError,
removeAssetPasswordError, removeAssetPasswordError,
setAssetPassword, onSetAssetPassword,
getAssetPassword getAssetPassword
}; };
}; };

View File

@ -13,7 +13,7 @@ export const AppPasswordAccess: React.FC<{
assetId: string; assetId: string;
type: ShareAssetType; type: ShareAssetType;
children: React.ReactNode; children: React.ReactNode;
}> = React.memo(({ children, assetId }) => { }> = React.memo(({ children, assetId, type }) => {
const getAssetPassword = useBusterAssetsContextSelector((state) => state.getAssetPassword); const getAssetPassword = useBusterAssetsContextSelector((state) => state.getAssetPassword);
const { password, error } = getAssetPassword(assetId); const { password, error } = getAssetPassword(assetId);
@ -21,7 +21,9 @@ export const AppPasswordAccess: React.FC<{
return <>{children}</>; return <>{children}</>;
} }
return <AppPasswordInputComponent password={password} error={error} assetId={assetId} />; return (
<AppPasswordInputComponent password={password} error={error} assetId={assetId} type={type} />
);
}); });
AppPasswordAccess.displayName = 'AppPasswordAccess'; AppPasswordAccess.displayName = 'AppPasswordAccess';
@ -30,12 +32,13 @@ const AppPasswordInputComponent: React.FC<{
password: string | undefined; password: string | undefined;
error: string | null; error: string | null;
assetId: string; assetId: string;
}> = ({ password, error, assetId }) => { type: ShareAssetType;
const setAssetPassword = useBusterAssetsContextSelector((state) => state.setAssetPassword); }> = ({ password, error, assetId, type }) => {
const onSetAssetPassword = useBusterAssetsContextSelector((state) => state.onSetAssetPassword);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const onEnterPassword = useMemoizedFn((v: string) => { const onEnterPassword = useMemoizedFn((v: string) => {
setAssetPassword(assetId, v); onSetAssetPassword(assetId, v, type);
}); });
const onPressEnter = useMemoizedFn((v: React.KeyboardEvent<HTMLInputElement>) => { const onPressEnter = useMemoizedFn((v: React.KeyboardEvent<HTMLInputElement>) => {
@ -72,12 +75,13 @@ const AppPasswordInputComponent: React.FC<{
className="w-full" className="w-full"
placeholder="Enter password" placeholder="Enter password"
type="password" type="password"
autoFocus
/> />
{error ? ( {/* {error ? (
<Text className="mb-1!" variant="danger"> <Text className="mb-1!" variant="danger">
{error} {error}
</Text> </Text>
) : null} ) : null} */}
</div> </div>
<Button block variant="black" onClick={onEnterButtonPress}> <Button block variant="black" onClick={onEnterButtonPress}>

View File

@ -1,16 +1,11 @@
'use server'; 'use client';
import React from 'react'; import React, { useMemo } from 'react';
import { BusterDashboardResponse, IBusterMetric, ShareAssetType } from '@/api/asset_interfaces'; import { ShareAssetType } from '@/api/asset_interfaces';
import { AppPasswordAccess } from '@/controllers/AppPasswordAccess'; import { AppPasswordAccess } from '@/controllers/AppPasswordAccess';
import { AppNoPageAccess } from '@/controllers/AppNoPageAccess'; import { AppNoPageAccess } from '@/controllers/AppNoPageAccess';
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'; import { useGetAsset } from './useGetAsset';
import { AppAssetLoadingContainer } from './AppAssetLoadingContainer'; import { FileIndeterminateLoader } from '@/components/features/FileIndeterminateLoader';
import { prefetchGetMetric } from '@/api/buster_rest/metrics/queryReqestsServer';
import { prefetchGetDashboard } from '@/api/buster_rest/dashboards/queryServerRequests';
import { metricsQueryKeys } from '@/api/query_keys/metric';
import { dashboardQueryKeys } from '@/api/query_keys/dashboard';
import { HydrationBoundaryAssetStore } from '@/context/Assets/HydrationBoundaryAssetStore';
export type AppAssetCheckLayoutProps = { export type AppAssetCheckLayoutProps = {
assetId: string; assetId: string;
@ -22,22 +17,21 @@ export const AppAssetCheckLayout: React.FC<
{ {
children: React.ReactNode; children: React.ReactNode;
} & AppAssetCheckLayoutProps } & AppAssetCheckLayoutProps
> = async ({ children, type, assetId, versionNumber }) => { > = React.memo(({ children, type, assetId, versionNumber }) => {
const queryClient = await prefetchAsset(assetId, type, versionNumber); const { hasAccess, passwordRequired, isPublic, isFetched, showLoader, error } = useGetAsset({
assetId,
type,
versionNumber
});
const { const Component = useMemo(() => {
has_access, if (!isFetched) return <></>;
password_required,
public: pagePublic,
queryData
} = getAssetAccess({ assetId, type, queryClient });
const Component = (() => { if (!hasAccess && !isPublic) {
if (!has_access && !pagePublic) {
return <AppNoPageAccess assetId={assetId} />; return <AppNoPageAccess assetId={assetId} />;
} }
if (pagePublic && password_required) { if (isPublic && passwordRequired) {
return ( return (
<AppPasswordAccess assetId={assetId} type={type as ShareAssetType}> <AppPasswordAccess assetId={assetId} type={type as ShareAssetType}>
{children} {children}
@ -46,98 +40,14 @@ export const AppAssetCheckLayout: React.FC<
} }
return <>{children}</>; return <>{children}</>;
})(); }, [isFetched, hasAccess, isPublic, passwordRequired, assetId, type, children]);
const dehydratedState = dehydrate(queryClient, {
shouldDehydrateQuery: () => true,
shouldRedactErrors: () => false
});
return ( return (
<HydrationBoundary state={dehydratedState}> <>
<HydrationBoundaryAssetStore asset={queryData}> {showLoader && <FileIndeterminateLoader />}
<AppAssetLoadingContainer assetId={assetId} type={type} versionNumber={versionNumber}> {Component}
{Component} </>
</AppAssetLoadingContainer>
</HydrationBoundaryAssetStore>
</HydrationBoundary>
); );
}; });
const prefetchAsset = async ( AppAssetCheckLayout.displayName = 'AppAssetCheckLayout';
assetId: string,
type: 'metric' | 'dashboard',
versionNumber?: number
) => {
let queryClient = new QueryClient();
switch (type) {
case 'metric':
queryClient = await prefetchGetMetric(
{ id: assetId, version_number: versionNumber },
queryClient
);
break;
case 'dashboard':
queryClient = await prefetchGetDashboard(
{ id: assetId, version_number: versionNumber },
queryClient
);
break;
default:
const _exhaustiveCheck: never = type;
}
return queryClient;
};
const getAssetAccess = ({
assetId,
type,
queryClient
}: {
assetId: string;
type: AppAssetCheckLayoutProps['type'];
queryClient: QueryClient;
}): {
has_access: boolean;
password_required: boolean;
public: boolean;
queryData?: IBusterMetric | BusterDashboardResponse | undefined;
} => {
const options =
type === 'metric'
? metricsQueryKeys.metricsGetMetric(assetId)
: dashboardQueryKeys.dashboardGetDashboard(assetId);
const queryState = queryClient.getQueryState(options.queryKey);
const queryData = queryClient.getQueryData(options.queryKey);
const error = queryState?.error;
if (!error) {
return {
has_access: true,
password_required: false,
public: false,
queryData
};
}
const status = error?.status;
if (status === 418) {
return {
has_access: false,
password_required: true,
public: true,
queryData
};
}
return {
has_access: false,
password_required: false,
public: false,
queryData
};
};

View File

@ -1,107 +0,0 @@
'use client';
import { useGetDashboard } from '@/api/buster_rest/dashboards';
import { useGetMetric, useGetMetricData } from '@/api/buster_rest/metrics';
import { FileIndeterminateLoader } from '@/components/features/FileIndeterminateLoader';
import React, { useMemo } from 'react';
export const AppAssetLoadingContainer: React.FC<{
assetId: string;
type: 'metric' | 'dashboard';
children: React.ReactNode;
versionNumber: number | undefined;
}> = React.memo(({ assetId, type, children, versionNumber }) => {
const {
isFetchedConfig: isFetchedMetricConfig,
isFetchedData: isFetchedMetricData,
error: metricError
} = useGetMetricAssetData({
assetId,
enabled: type === 'metric',
versionNumber
});
const {
isFetchedConfig: isFetchedDashboardConfig,
isFetchedData: isFetchedDashboardData,
error: dashboardError
} = useGetDashboardAssetData({
assetId,
enabled: type === 'dashboard',
versionNumber
});
const showLoader = useMemo(() => {
if (type === 'metric') {
return (!isFetchedMetricConfig || !isFetchedMetricData) && !metricError;
}
if (type === 'dashboard') {
return (!isFetchedDashboardConfig || !isFetchedDashboardData) && !dashboardError;
}
return true;
}, [
isFetchedMetricConfig,
isFetchedMetricData,
isFetchedDashboardConfig,
isFetchedDashboardData,
metricError,
dashboardError,
type
]);
return (
<>
{showLoader && <FileIndeterminateLoader />}
{children}
</>
);
});
AppAssetLoadingContainer.displayName = 'AppAssetLoadingContainer';
const useGetMetricAssetData = ({
assetId,
enabled,
versionNumber
}: {
assetId: string;
enabled: boolean;
versionNumber: number | undefined;
}) => {
const { isFetched: isMetricFetched, ...rest } = useGetMetric({
id: enabled ? assetId : undefined,
versionNumber
});
const { isFetched: isMetricDataFetched } = useGetMetricData({
id: enabled ? assetId : undefined,
versionNumber
});
return {
isFetchedConfig: isMetricFetched,
isFetchedData: isMetricDataFetched,
error: rest.error
};
};
const useGetDashboardAssetData = ({
assetId,
enabled,
versionNumber
}: {
assetId: string;
enabled: boolean;
versionNumber: number | undefined;
}) => {
const { isFetched: isDashboardFetched, error: dashboardError } = useGetDashboard({
id: enabled ? assetId : undefined,
versionNumber
});
return {
isFetchedConfig: isDashboardFetched,
isFetchedData: isDashboardFetched,
error: dashboardError
};
};

View File

@ -0,0 +1,129 @@
import { useGetMetric, useGetMetricData } from '@/api/buster_rest/metrics';
import { useGetDashboard } from '@/api/buster_rest/dashboards';
import { RustApiError } from '@/api/buster_rest/errors';
interface BaseGetAssetProps {
assetId: string;
}
interface MetricAssetProps extends BaseGetAssetProps {
type: 'metric';
versionNumber?: number;
}
interface DashboardAssetProps extends BaseGetAssetProps {
type: 'dashboard';
}
type UseGetAssetProps = MetricAssetProps | DashboardAssetProps;
type UseGetAssetReturn<T extends UseGetAssetProps> = {
isFetched: boolean;
error: RustApiError | null;
hasAccess: boolean;
passwordRequired: boolean;
isPublic: boolean;
showLoader: boolean;
};
export const useGetAsset = (props: UseGetAssetProps): UseGetAssetReturn<typeof props> => {
//metric
const {
error: errorMetric,
isError: isErrorMetric,
dataUpdatedAt,
isFetched: isFetchedMetric
} = useGetMetric(
{
id: props.assetId,
versionNumber: props.type === 'metric' ? props.versionNumber : undefined
},
{
enabled: props.type === 'metric' && !!props.assetId
}
);
const { isFetched: isFetchedMetricData } = useGetMetricData(
{
id: props.assetId,
versionNumber: props.type === 'metric' ? props.versionNumber : undefined
},
{
enabled:
props.type === 'metric' &&
!!props.assetId &&
isFetchedMetric &&
!!dataUpdatedAt && //This is a hack to prevent the query from being run when the asset is not fetched.
!isErrorMetric
}
);
//dashboard
const { isFetched: isFetchedDashboard, error: errorDashboard } = useGetDashboard(
{
id: props.type === 'dashboard' ? props.assetId : undefined
},
{ enabled: props.type === 'dashboard' && !!props.assetId }
);
const { hasAccess, passwordRequired, isPublic } = getAssetAccess({
error: props.type === 'metric' ? errorMetric : errorDashboard
});
if (props.type === 'metric') {
return {
isFetched: isFetchedMetric,
error: errorMetric,
hasAccess,
passwordRequired,
isPublic,
showLoader: (!isFetchedMetricData || !!errorMetric) && !isFetchedMetric
};
}
const exhaustiveCheck: 'dashboard' = props.type;
return {
isFetched: isFetchedDashboard,
error: errorDashboard,
hasAccess,
passwordRequired,
isPublic,
showLoader: isFetchedDashboard || !!errorDashboard
};
};
const getAssetAccess = ({
error
}: {
error: RustApiError | null;
}): {
hasAccess: boolean;
passwordRequired: boolean;
isPublic: boolean;
} => {
if (!error) {
return {
hasAccess: true,
passwordRequired: false,
isPublic: false
};
}
const status = error?.status;
console.log(error, status);
if (status === 418) {
return {
hasAccess: false,
passwordRequired: true,
isPublic: true
};
}
return {
hasAccess: false,
passwordRequired: false,
isPublic: false
};
};