mirror of https://github.com/buster-so/buster.git
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:
commit
c1832d4c19
|
@ -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",
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export type * from './interfaces';
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -53,3 +53,8 @@ p {
|
|||
th {
|
||||
@apply text-base font-normal;
|
||||
}
|
||||
|
||||
strong,
|
||||
b {
|
||||
@apply font-semibold;
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(),
|
||||
})
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue