Merge pull request #1141 from buster-so/big-nate-bus-1935-copying-project-ids-from-buster-reports-results-in-empty

Big nate bus 1935 copying project ids from buster reports results in empty
This commit is contained in:
Nate Kelley 2025-09-25 11:27:12 -06:00 committed by GitHub
commit c1832d4c19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 104 additions and 65 deletions

View File

@ -127,6 +127,7 @@
"@tiptap/react": "^3.5.0",
"@tiptap/starter-kit": "^3.5.0",
"@tiptap/suggestion": "^3.5.0",
"@types/qs": "^6.14.0",
"@udecode/cn": "^49.0.15",
"@uploadthing/react": "^7.3.3",
"axios": "catalog:",
@ -157,6 +158,7 @@
"platejs": "49.2.21",
"pluralize": "^8.0.0",
"posthog-js": "^1.268.1",
"qs": "^6.14.0",
"react": "catalog:",
"react-colorful": "^5.6.1",
"react-day-picker": "^8.10.1",

View File

@ -7,7 +7,6 @@ export type * from './datasources';
export type * from './metric';
export type * from './permission';
export type * from './permission_groups';
export type * from './search';
export type * from './sql';
export type * from './teams';
export type * from './terms';

View File

@ -1 +0,0 @@
export type * from './interfaces';

View File

@ -1,10 +0,0 @@
import type { ShareAssetType } from '@buster/server-shared/share';
export interface BusterSearchResult {
id: string;
highlights: string[];
name: string;
updated_at: string;
type: ShareAssetType;
score: number;
}

View File

@ -1,15 +1,22 @@
import type { SearchTextResponse } from '@buster/server-shared/search';
import { keepPreviousData, type UseQueryOptions, useQuery } from '@tanstack/react-query';
import type { RustApiError } from '@/api/errors';
import { searchQueryKeys } from '@/api/query_keys/search';
import { search } from './requests';
export const useSearch = (
export const useSearch = <T = SearchTextResponse>(
params: Parameters<typeof search>[0],
options?: Omit<UseQueryOptions<Awaited<ReturnType<typeof search>>>, 'queryKey' | 'queryFn'>
options?: Omit<UseQueryOptions<SearchTextResponse, RustApiError, T>, 'queryKey' | 'queryFn'>,
postQueryOptions?: {
doNotUnwrapData?: boolean;
}
) => {
return useQuery({
const { doNotUnwrapData = false } = postQueryOptions || {};
return useQuery<SearchTextResponse, RustApiError, T>({
...searchQueryKeys.getSearchResult(params),
queryFn: () => search(params),
placeholderData: keepPreviousData,
select: options?.select,
...options,
placeholderData: keepPreviousData,
});
};

View File

@ -1,11 +1,7 @@
import type { AssetType } from '@buster/server-shared/assets';
import type { BusterSearchResult } from '@/api/asset_interfaces/search';
import { mainApi } from '../instances';
import type { SearchTextRequest, SearchTextResponse } from '@buster/server-shared/search';
import qs from 'qs';
import { mainApiV2 } from '../instances';
export const search = async (params: {
query: string;
asset_types: Extract<AssetType, 'dashboard_file' | 'metric_file' | 'collection'>[];
num_results?: number;
}) => {
return mainApi.post<BusterSearchResult[]>('/search', params).then((res) => res.data);
export const search = async (params: SearchTextRequest) => {
return mainApiV2.get<SearchTextResponse>('/search', { params }).then((res) => res.data);
};

View File

@ -1,6 +1,7 @@
import { isServer } from '@tanstack/react-query';
import type { AxiosRequestHeaders } from 'axios';
import axios, { type AxiosError, type InternalAxiosRequestConfig } from 'axios';
import qs from 'qs';
import { getSupabaseSession } from '@/integrations/supabase/getSupabaseUserClient';
import { Route as AuthRoute } from '@/routes/auth.login';
import { BASE_URL_V2 } from './config';
@ -15,6 +16,9 @@ export const createAxiosInstance = (baseURL = BASE_URL_V2) => {
headers: {
'Content-Type': 'application/json',
},
paramsSerializer: (params) => {
return qs.stringify(params, { arrayFormat: 'repeat' }); //💰
},
});
// Response interceptor with retry logic for auth errors

View File

@ -1,9 +1,8 @@
import type { SearchTextRequest, SearchTextResponse } from '@buster/server-shared';
import { queryOptions } from '@tanstack/react-query';
import type { BusterSearchResult } from '@/api/asset_interfaces/search';
import type { search } from '../buster_rest/search';
export const getSearchResult = (params: Parameters<typeof search>[0]) =>
queryOptions<BusterSearchResult[]>({
export const getSearchResult = (params: SearchTextRequest) =>
queryOptions<SearchTextResponse>({
queryKey: ['search', 'results', params] as const,
staleTime: 1000 * 15, // 15 seconds,
});

View File

@ -1,6 +1,6 @@
import type { SearchTextResponse } from '@buster/server-shared/search';
import type { ShareAssetType } from '@buster/server-shared/share';
import React, { useLayoutEffect, useMemo, useState } from 'react';
import type { BusterSearchResult } from '@/api/asset_interfaces/search';
import {
useAddAndRemoveAssetsFromCollection,
useGetCollection,
@ -33,8 +33,9 @@ export const AddToCollectionModal: React.FC<{
const { data: searchResults } = useSearch({
query: debouncedSearchTerm,
asset_types: ['metric_file', 'dashboard_file'],
num_results: 100,
assetTypes: ['metric_file', 'dashboard_file'],
page_size: 50,
page: 1,
});
const [selectedAssets, setSelectedAssets] = useState<SelectedAsset[]>([]);
@ -42,13 +43,13 @@ export const AddToCollectionModal: React.FC<{
return selectedAssets.map((asset) => asset.id);
}, [selectedAssets]);
const columns = useMemo<InputSelectModalProps<BusterSearchResult>['columns']>(
const columns = useMemo<InputSelectModalProps<SearchTextResponse['data'][number]>['columns']>(
() => [
{
title: 'Name',
dataIndex: 'name',
render: (name, data) => {
const Icon = assetTypeToIcon(data.type) || ASSET_ICONS.metrics;
dataIndex: 'title',
render: (name, record) => {
const Icon = assetTypeToIcon(record.assetType) || ASSET_ICONS.metrics;
return (
<div className="flex items-center gap-1.5">
<span className="text-icon-color">
@ -61,7 +62,7 @@ export const AddToCollectionModal: React.FC<{
},
{
title: 'Updated',
dataIndex: 'updated_at',
dataIndex: 'updatedAt',
width: 140,
render: (value: string) => {
return formatDate({
@ -74,10 +75,10 @@ export const AddToCollectionModal: React.FC<{
[]
);
const rows: BusterListRowItem<BusterSearchResult>[] = useMemo(() => {
const rows: BusterListRowItem<SearchTextResponse['data'][number]>[] = useMemo(() => {
return (
searchResults?.map((asset) => ({
id: asset.id,
searchResults?.data?.map((asset) => ({
id: asset.assetId,
data: asset,
})) || []
);
@ -86,7 +87,7 @@ export const AddToCollectionModal: React.FC<{
const createKeySearchResultMap = useMemoizedFn(() => {
const map = new Map<string, SelectedAsset>();
rows.forEach((asset) => {
if (asset.data?.type) map.set(asset.id, { type: asset.data?.type, id: asset.id });
if (asset.data?.assetType) map.set(asset.id, { type: asset.data?.assetType, id: asset.id });
});
return map;
});

View File

@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useState } from 'react';
import type { BusterSearchResult } from '@/api/asset_interfaces/search';
import type { SearchTextRequest, SearchTextResponse } from '@buster/server-shared/search';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useSearch } from '@/api/buster_rest/search';
import { Button } from '@/components/ui/buttons';
import {
@ -9,6 +9,7 @@ import {
import { useDebounce } from '@/hooks/useDebounce';
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
import { formatDate } from '@/lib/date';
import { assetTypeToIcon } from '../icons/assetIcons';
export const AddMetricModal: React.FC<{
open: boolean;
@ -46,21 +47,34 @@ export const AddMetricModal: React.FC<{
const { data: searchResults } = useSearch(
{
query: debouncedSearchTerm,
asset_types: ['metric_file'],
num_results: 100,
},
{ enabled: true }
assetTypes: ['metric_file'],
page_size: 25,
page: 1,
} satisfies SearchTextRequest,
{ enabled: true, select: useCallback((data: SearchTextResponse) => data.data, []) }
);
const columns = useMemo<InputSelectModalProps<BusterSearchResult>['columns']>(
const columns = useMemo<InputSelectModalProps<SearchTextResponse['data'][number]>['columns']>(
() => [
{
title: 'Name',
dataIndex: 'name',
title: 'Title',
dataIndex: 'title',
render: (value, record) => {
const Icon = assetTypeToIcon(record.assetType);
return (
<div className="flex items-center gap-1.5">
<span className="text-icon-color">
<Icon />
</span>
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: this endpoint is sanitized */}
<span dangerouslySetInnerHTML={{ __html: value }}></span>
</div>
);
},
},
{
title: 'Updated',
dataIndex: 'updated_at',
title: 'Updated at',
dataIndex: 'updatedAt',
width: 140,
render: (value) => {
return formatDate({
@ -76,8 +90,8 @@ export const AddMetricModal: React.FC<{
const rows = useMemo(() => {
return (
searchResults?.map((result) => ({
id: result.id,
dataTestId: `item-${result.id}`,
id: result.assetId,
dataTestId: `item-${result.assetId}`,
data: result,
})) || []
);
@ -100,7 +114,7 @@ export const AddMetricModal: React.FC<{
const item = rows.find((row) => row.id === id);
return {
id: id,
name: item?.data?.name || id,
name: item?.data?.title || id,
};
});

View File

@ -11,7 +11,6 @@ import {
SquareChart,
Table,
} from '@/components/ui/icons';
import type { iconProps } from '@/components/ui/icons/NucleoIconOutlined/iconProps';
import SquareChartPlus from '@/components/ui/icons/NucleoIconOutlined/square-chart-plus';
export const ASSET_ICONS = {

View File

@ -16,7 +16,6 @@ export type MetricChartCardProps = {
metricId: string;
versionNumber: number | undefined;
readOnly?: boolean;
className?: string;
attributes?: DraggableAttributes;
listeners?: DraggableSyntheticListeners;
headerSecondaryContent?: React.ReactNode;
@ -25,7 +24,7 @@ export type MetricChartCardProps = {
renderChartContent?: boolean; // we do this to avoid expensive rendering if off screen
disableTooltip?: boolean;
cacheDataId?: string;
};
} & React.HTMLAttributes<HTMLDivElement>;
const stableMetricSelect = ({
chart_config,
@ -62,6 +61,7 @@ export const MetricChartCard = React.memo(
renderChartContent = true,
disableTooltip,
cacheDataId,
...rest
},
ref
) => {
@ -102,6 +102,7 @@ export const MetricChartCard = React.memo(
errorData={errorData}
isTable={isTable}
className={className}
{...rest}
>
<MetricViewChartHeader
name={name}
@ -144,13 +145,12 @@ type MetricViewChartCardContainerProps = {
hasData: boolean;
errorData: boolean;
isTable: boolean;
className?: string;
};
} & React.HTMLAttributes<HTMLDivElement>;
const MetricViewChartCardContainer = React.forwardRef<
HTMLDivElement,
MetricViewChartCardContainerProps
>(({ children, loadingData, hasData, errorData, isTable, className }, ref) => {
>(({ children, loadingData, hasData, errorData, isTable, className, ...divProps }, ref) => {
const cardClass = React.useMemo(() => {
if (loadingData || errorData || !hasData) return 'h-full max-h-[600px]';
if (isTable) return 'h-full';
@ -161,6 +161,7 @@ const MetricViewChartCardContainer = React.forwardRef<
<MetricViewChartProvider>
<div
ref={ref}
{...divProps}
className={cn(
'bg-background flex flex-col overflow-hidden rounded border shadow',
cardClass,

View File

@ -3,6 +3,7 @@ import { MetricChartCard } from '@/components/features/metrics/MetricChartCard';
import { ReportMetricThreeDotMenu } from '@/components/features/metrics/ReportMetricItem';
import { useGetReportParams } from '@/context/Reports/useGetReportParams';
import { useInViewport } from '@/hooks/useInViewport';
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
export const MetricContent = React.memo(
({
@ -30,6 +31,10 @@ export const MetricContent = React.memo(
}
const renderChart = inViewport || isExportMode || hasBeenInViewport.current;
const handleCopy = useMemoizedFn((e: React.ClipboardEvent<HTMLDivElement>) => {
e.stopPropagation();
});
return (
<MetricChartCard
ref={ref}
@ -50,6 +55,7 @@ export const MetricContent = React.memo(
/>
)
}
onCopy={handleCopy}
/>
);
}

View File

@ -53,3 +53,8 @@ p {
th {
@apply text-base font-normal;
}
strong,
b {
@apply font-semibold;
}

View File

@ -1,8 +1,9 @@
import z from 'zod';
import { AssetTypeSchema } from './asset';
export const TextSearchResultSchema = z.object({
assetId: z.string().uuid(),
assetType: z.string(),
assetType: AssetTypeSchema,
title: z.string(),
additionalText: z.string().nullable(),
updatedAt: z.string().datetime(),

View File

@ -99,6 +99,10 @@
"./healthcheck": {
"types": "./dist/healthcheck/index.d.ts",
"default": "./dist/healthcheck/index.js"
},
"./search": {
"types": "./dist/search/index.d.ts",
"default": "./dist/search/index.js"
}
},
"module": "src/index.ts",

View File

@ -28,7 +28,8 @@ export const SearchTextRequestSchema = z
}
return Boolean(val);
}, z.boolean())
.default(false),
.default(false)
.optional(),
endDate: z.string().datetime().optional(),
startDate: z.string().datetime().optional(),
})

View File

@ -18,6 +18,6 @@ export const PaginatedResponseSchema = <T>(schema: z.ZodType<T>) =>
export type PaginatedResponse<T> = z.infer<ReturnType<typeof PaginatedResponseSchema<T>>>;
export const PaginatedRequestSchema = z.object({
page: z.coerce.number().min(1).default(1),
page: z.coerce.number().min(1).optional().default(1),
page_size: z.coerce.number().min(1).max(5000).default(250),
});

View File

@ -703,6 +703,9 @@ importers:
'@tiptap/suggestion':
specifier: ^3.5.0
version: 3.5.0(@tiptap/core@3.5.0(@tiptap/pm@3.5.0))(@tiptap/pm@3.5.0)
'@types/qs':
specifier: ^6.14.0
version: 6.14.0
'@udecode/cn':
specifier: ^49.0.15
version: 49.0.15(@types/react@19.1.13)(class-variance-authority@0.7.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(tailwind-merge@3.3.1)
@ -793,6 +796,9 @@ importers:
posthog-js:
specifier: ^1.268.1
version: 1.268.1
qs:
specifier: ^6.14.0
version: 6.14.0
react:
specifier: 'catalog:'
version: 19.1.1
@ -6356,6 +6362,9 @@ packages:
'@types/prismjs@1.26.5':
resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
'@types/qs@6.14.0':
resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==}
'@types/react-dom@19.1.9':
resolution: {integrity: sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==}
peerDependencies:
@ -19323,6 +19332,8 @@ snapshots:
'@types/prismjs@1.26.5': {}
'@types/qs@6.14.0': {}
'@types/react-dom@19.1.9(@types/react@19.1.13)':
dependencies:
'@types/react': 19.1.13
@ -19591,7 +19602,7 @@ snapshots:
sirv: 3.0.1
tinyglobby: 0.2.14
tinyrainbow: 2.0.0
vitest: 3.2.4(@edge-runtime/vm@3.2.0)(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(msw@2.11.3(@types/node@22.18.1)(typescript@5.9.2))(sass@1.93.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)
vitest: 3.2.4(@edge-runtime/vm@3.2.0)(@types/debug@4.1.12)(@types/node@24.3.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(msw@2.11.3(@types/node@24.3.1)(typescript@5.9.2))(sass@1.93.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)
'@vitest/utils@3.2.4':
dependencies: