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';
export const rustErrorHandler = (errors: any = {}): RustApiError => {
const data = errors?.response?.data;
const status = errors?.status;
if (data && isString(data)) {
return { message: String(data) };
return { message: String(data), status };
}
if (data && data?.message) {
return { message: String(data.message) };
return { message: String(data.message), status };
}
if (data && data?.detail) {
if (typeof data.detail === 'string') {
return { message: String(data.detail) };
return { message: String(data.detail), status };
}
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) {
return { message: String(errors.message) };
return { message: String(errors.message), status };
}
if (typeof errors === 'string') {
return { message: String(errors) };
return { message: String(errors), status };
}
return {};

View File

@ -21,7 +21,7 @@ import { collectionQueryKeys } from '@/api/query_keys/collection';
import { useMemo } from 'react';
import { useBusterAssetsContextSelector } from '@/context/Assets/BusterAssetsProvider';
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 {
useAddAssetToCollection,
@ -81,7 +81,7 @@ export const useGetMetric = <TData = IBusterMetric>(
return useQuery({
...options,
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) {
if (error?.message !== undefined) {
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.
*/
export const useGetMetricData = ({
export const useGetMetricData = <TData = BusterMetricData>(
{
id,
versionNumber: versionNumberProp
}: {
id: string | undefined;
versionNumber?: number;
}) => {
},
params?: Omit<UseQueryOptions<BusterMetricData, RustApiError, TData>, 'queryKey' | 'queryFn'>
) => {
const getAssetPassword = useBusterAssetsContextSelector((x) => x.getAssetPassword);
const { password } = getAssetPassword(id!);
const {
isFetched: isFetchedMetric,
error: errorMetric,
@ -141,7 +147,7 @@ export const useGetMetricData = ({
}, [versionNumberProp, versionNumberFromParams]);
const queryFn = useMemoizedFn(() => {
return getMetricData({ id: id!, version_number: versionNumber });
return getMetricData({ id: id!, version_number: versionNumber, password });
});
return useQuery({
@ -149,7 +155,9 @@ export const useGetMetricData = ({
queryFn,
enabled: () => {
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 ({
id,
version_number
version_number,
password
}: {
id: string;
version_number?: number;
password?: string;
}) => {
return mainApi
.get<BusterMetricData>(`/metrics/${id}/data`, { params: { version_number } })
.get<BusterMetricData>(`/metrics/${id}/data`, { params: { password, version_number } })
.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)}>
<Button
block
loading={isPublishing}
onClick={async (v) => {
onTogglePublish(false);
}}>

View File

@ -1,19 +1,58 @@
'use client';
import type { ShareAssetType } from '@/api/asset_interfaces/share';
import { queryKeys } from '@/api/query_keys';
import { useMemoizedFn } from '@/hooks';
import { timeout } from '@/lib';
import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import { createContext, useContextSelector } from 'use-context-selector';
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 setAssetPassword = useMemoizedFn((assetId: string, password: string) => {
setAssetsToPasswords((prev) => ({ ...prev, [assetId]: password }));
removeAssetPasswordError(assetId);
const invalidateAssetData = useMemoizedFn(async (assetId: string, type: ShareAssetType) => {
if (type === 'metric') {
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(
(
@ -21,9 +60,11 @@ const useBusterAssets = () => {
): {
password: undefined | string;
error: string | null;
type: ShareAssetType | undefined;
} => {
return {
password: assetsToPasswords[assetId] || undefined,
password: assetsToPasswords[assetId]?.password || undefined,
type: assetsToPasswords[assetId]?.type || undefined,
error: assetsPasswordErrors[assetId] || null
};
},
@ -43,7 +84,7 @@ const useBusterAssets = () => {
return {
setAssetPasswordError,
removeAssetPasswordError,
setAssetPassword,
onSetAssetPassword,
getAssetPassword
};
};

View File

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

View File

@ -1,16 +1,11 @@
'use server';
'use client';
import React from 'react';
import { BusterDashboardResponse, IBusterMetric, ShareAssetType } from '@/api/asset_interfaces';
import React, { useMemo } from 'react';
import { ShareAssetType } from '@/api/asset_interfaces';
import { AppPasswordAccess } from '@/controllers/AppPasswordAccess';
import { AppNoPageAccess } from '@/controllers/AppNoPageAccess';
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
import { AppAssetLoadingContainer } from './AppAssetLoadingContainer';
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';
import { useGetAsset } from './useGetAsset';
import { FileIndeterminateLoader } from '@/components/features/FileIndeterminateLoader';
export type AppAssetCheckLayoutProps = {
assetId: string;
@ -22,22 +17,21 @@ export const AppAssetCheckLayout: React.FC<
{
children: React.ReactNode;
} & AppAssetCheckLayoutProps
> = async ({ children, type, assetId, versionNumber }) => {
const queryClient = await prefetchAsset(assetId, type, versionNumber);
> = React.memo(({ children, type, assetId, versionNumber }) => {
const { hasAccess, passwordRequired, isPublic, isFetched, showLoader, error } = useGetAsset({
assetId,
type,
versionNumber
});
const {
has_access,
password_required,
public: pagePublic,
queryData
} = getAssetAccess({ assetId, type, queryClient });
const Component = useMemo(() => {
if (!isFetched) return <></>;
const Component = (() => {
if (!has_access && !pagePublic) {
if (!hasAccess && !isPublic) {
return <AppNoPageAccess assetId={assetId} />;
}
if (pagePublic && password_required) {
if (isPublic && passwordRequired) {
return (
<AppPasswordAccess assetId={assetId} type={type as ShareAssetType}>
{children}
@ -46,98 +40,14 @@ export const AppAssetCheckLayout: React.FC<
}
return <>{children}</>;
})();
const dehydratedState = dehydrate(queryClient, {
shouldDehydrateQuery: () => true,
shouldRedactErrors: () => false
});
}, [isFetched, hasAccess, isPublic, passwordRequired, assetId, type, children]);
return (
<HydrationBoundary state={dehydratedState}>
<HydrationBoundaryAssetStore asset={queryData}>
<AppAssetLoadingContainer assetId={assetId} type={type} versionNumber={versionNumber}>
<>
{showLoader && <FileIndeterminateLoader />}
{Component}
</AppAssetLoadingContainer>
</HydrationBoundaryAssetStore>
</HydrationBoundary>
</>
);
};
});
const prefetchAsset = async (
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
};
};
AppAssetCheckLayout.displayName = 'AppAssetCheckLayout';

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
};
};