UNPKG

drizzle-cube

Version:

Drizzle ORM-first semantic layer with Cube.js compatibility. Type-safe analytics and dashboards with SQL injection protection.

514 lines (513 loc) 18.5 kB
import { MouseEvent, DragEvent } from 'react'; import { CubeQuery, Filter, ChartType, ChartAxisConfig, ChartDisplayConfig, MultiQueryConfig } from '../../types'; import { ColorPalette } from '../../utils/colorPalettes'; import { MetaResponse, MetaField, MetaCube, QueryAnalysis } from '../../shared/types'; import { ChartAvailabilityMap } from '../../shared/chartDefaults'; import { MultiQueryValidationResult } from '../../utils/multiQueryValidation'; export type { MetaResponse, MetaField, MetaCube, QueryAnalysis }; /** * A selected metric (measure) with a letter label (A, B, C, ...) */ export interface MetricItem { /** Unique identifier for this metric selection */ id: string; /** Full field name, e.g., "Employees.count" */ field: string; /** Display label (A, B, C, ...) */ label: string; } /** * A selected breakdown (dimension or time dimension) */ export interface BreakdownItem { /** Unique identifier for this breakdown selection */ id: string; /** Full field name, e.g., "Employees.departmentName" */ field: string; /** Granularity for time dimensions (day, week, month, quarter, year) */ granularity?: string; /** Whether this is a time dimension */ isTimeDimension: boolean; /** Enable period comparison for time dimensions (compares current filter period vs prior period) */ enableComparison?: boolean; } /** Validation status for query building */ export type ValidationStatus = 'idle' | 'validating' | 'valid' | 'invalid'; /** Execution status for query results */ export type ExecutionStatus = 'idle' | 'loading' | 'refreshing' | 'success' | 'error'; /** * Main state for the AnalysisBuilder component */ export interface AnalysisBuilderState { /** Selected metrics (measures) */ metrics: MetricItem[]; /** Selected breakdowns (dimensions and time dimensions) */ breakdowns: BreakdownItem[]; /** Applied filters */ filters: Filter[]; /** Sort order for this query (field name -> 'asc' | 'desc') */ order?: Record<string, 'asc' | 'desc'>; validationStatus: ValidationStatus; validationError: string | null; executionStatus: ExecutionStatus; executionResults: any[] | null; executionError: string | null; totalRowCount: number | null; resultsStale: boolean; } /** * State for the AI query generation panel */ export interface AIState { /** Whether the AI panel is open */ isOpen: boolean; /** User's natural language prompt */ userPrompt: string; /** Whether a query is being generated */ isGenerating: boolean; /** Error message from generation */ error: string | null; /** Whether the AI has generated a query that's been loaded */ hasGeneratedQuery: boolean; /** Snapshot of state before AI was opened (for undo) */ previousState: { metrics: MetricItem[]; breakdowns: BreakdownItem[]; filters: Filter[]; chartType: ChartType; chartConfig: ChartAxisConfig; displayConfig: ChartDisplayConfig; } | null; } /** * Mode for the field search modal - determines which field types are shown */ export type FieldSearchMode = 'metrics' | 'breakdown' | 'filter'; /** * Field type categorization */ export type FieldType = 'measure' | 'dimension' | 'timeDimension'; /** * A field option for display in the search modal */ export interface FieldOption { /** Full field name, e.g., "Employees.count" */ name: string; /** Display title */ title: string; /** Short title for compact display */ shortTitle: string; /** Field type (count, sum, avg, string, time, number, etc.) */ type: string; /** Optional description */ description?: string; /** Parent cube name */ cubeName: string; /** Categorized field type */ fieldType: FieldType; } /** * Props for the FieldSearchModal component */ export interface FieldSearchModalProps { /** Whether the modal is open */ isOpen: boolean; /** Callback to close the modal */ onClose: () => void; /** Callback when a field is selected. keepOpen=true when shift-click multi-selecting */ onSelect: (field: MetaField, fieldType: FieldType, cubeName: string, keepOpen?: boolean) => void; /** Mode determines which field types to show */ mode: FieldSearchMode; /** Schema metadata */ schema: MetaResponse | null; /** Already selected field names (to show checkmarks) */ selectedFields: string[]; /** Recently used field names */ recentFields?: string[]; } /** * Props for the FieldSearchItem component */ export interface FieldSearchItemProps { /** Field data */ field: FieldOption; /** Whether this field is selected */ isSelected: boolean; /** Whether this field is focused/highlighted */ isFocused: boolean; /** Click handler - receives mouse event for shift-click multi-select */ onClick: (e: MouseEvent) => void; /** Mouse enter handler (for detail panel) */ onMouseEnter: () => void; } /** * Props for the FieldDetailPanel component */ export interface FieldDetailPanelProps { /** Field to display details for */ field: FieldOption | null; } /** * Tab options for the query panel */ export type QueryPanelTab = 'query' | 'chart' | 'display'; /** * Props for the AnalysisQueryPanel component */ export interface AnalysisQueryPanelProps { /** Selected metrics */ metrics: MetricItem[]; /** Selected breakdowns */ breakdowns: BreakdownItem[]; /** Applied filters */ filters: Filter[]; /** Schema metadata */ schema: MetaResponse | null; /** Currently active tab */ activeTab: QueryPanelTab; /** Callback when active tab changes */ onActiveTabChange: (tab: QueryPanelTab) => void; onAddMetric: () => void; onRemoveMetric: (id: string) => void; onReorderMetrics?: (fromIndex: number, toIndex: number) => void; onAddBreakdown: () => void; onRemoveBreakdown: (id: string) => void; onBreakdownGranularityChange: (id: string, granularity: string) => void; onBreakdownComparisonToggle?: (id: string) => void; onReorderBreakdowns?: (fromIndex: number, toIndex: number) => void; onFiltersChange: (filters: Filter[]) => void; onDropFieldToFilter?: (field: string) => void; /** Current sort order */ order?: Record<string, 'asc' | 'desc'>; /** Callback when sort order changes */ onOrderChange: (fieldName: string, direction: 'asc' | 'desc' | null) => void; chartType: ChartType; chartConfig: ChartAxisConfig; displayConfig: ChartDisplayConfig; /** Color palette for display config options */ colorPalette?: ColorPalette; /** Map of chart type availability for disabling unavailable chart types */ chartAvailability?: ChartAvailabilityMap; onChartTypeChange: (type: ChartType) => void; onChartConfigChange: (config: ChartAxisConfig) => void; onDisplayConfigChange: (config: ChartDisplayConfig) => void; validationStatus: ValidationStatus; validationError: string | null; /** Number of queries (determines single vs multi-query display) */ queryCount?: number; /** Index of the currently active query tab */ activeQueryIndex?: number; /** Strategy for merging results from multiple queries */ mergeStrategy?: 'concat' | 'merge'; /** Callback when active query tab changes */ onActiveQueryChange?: (index: number) => void; /** Callback to add a new query */ onAddQuery?: () => void; /** Callback to remove a query at specified index */ onRemoveQuery?: (index: number) => void; /** Callback when merge strategy changes */ onMergeStrategyChange?: (strategy: 'concat' | 'merge') => void; /** Whether breakdowns are locked (synced from Q1 in merge mode) */ breakdownsLocked?: boolean; /** Combined metrics from all queries (for chart config in multi-query mode) */ combinedMetrics?: MetricItem[]; /** Combined breakdowns from all queries (for chart config in multi-query mode) */ combinedBreakdowns?: BreakdownItem[]; /** Validation result for multi-query mode (errors and warnings) */ multiQueryValidation?: MultiQueryValidationResult | null; } /** * Props for the AnalysisResultsPanel component */ export interface AnalysisResultsPanelProps { /** Current execution status */ executionStatus: ExecutionStatus; /** Execution results (raw data) */ executionResults: any[] | null; /** Execution error message */ executionError: string | null; /** Total row count (before limit) */ totalRowCount: number | null; /** Whether results are stale (query changed) */ resultsStale: boolean; /** Chart type for visualization */ chartType: ChartType; /** Chart axis configuration */ chartConfig: ChartAxisConfig; /** Chart display configuration */ displayConfig: ChartDisplayConfig; /** Color palette for charts */ colorPalette?: ColorPalette; /** Current palette name (for selector) */ currentPaletteName?: string; /** Callback when color palette changes (shows selector when provided) */ onColorPaletteChange?: (paletteName: string) => void; /** All queries for multi-query mode (used for table column headers per-query) */ allQueries?: CubeQuery[]; /** Schema metadata */ schema: MetaResponse | null; /** Active view (table or chart) */ activeView: 'table' | 'chart'; /** Callback when active view changes */ onActiveViewChange: (view: 'table' | 'chart') => void; /** Display limit for table */ displayLimit: number; /** Callback when display limit changes */ onDisplayLimitChange: (limit: number) => void; /** Whether the query has metrics (measures) - needed to enable/disable chart view */ hasMetrics: boolean; /** Debug data for each query (SQL, analysis, loading/error state) */ debugDataPerQuery?: Array<{ sql: { sql: string; params: any[]; } | null; analysis: QueryAnalysis | null; loading: boolean; error: string | null; }>; onShareClick?: () => void; canShare?: boolean; shareButtonState?: 'idle' | 'copied' | 'copied-no-chart'; onClearClick?: () => void; canClear?: boolean; enableAI?: boolean; isAIOpen?: boolean; onAIToggle?: () => void; /** Number of queries (for showing Table 1, Table 2 tabs) */ queryCount?: number; /** Per-query results (for table view in multi-query mode) */ perQueryResults?: (any[] | null)[]; /** Active table index in multi-query mode */ activeTableIndex?: number; /** Callback when active table changes */ onActiveTableChange?: (index: number) => void; } /** * Props for the MetricsSection component */ export interface MetricsSectionProps { /** Selected metrics */ metrics: MetricItem[]; /** Schema for resolving field titles */ schema: MetaResponse | null; /** Add metric handler */ onAdd: () => void; /** Remove metric handler */ onRemove: (id: string) => void; /** Whether the section is expanded */ isExpanded?: boolean; /** Toggle expansion */ onToggleExpanded?: () => void; /** Current sort order */ order?: Record<string, 'asc' | 'desc'>; /** Callback when sort order changes */ onOrderChange?: (fieldName: string, direction: 'asc' | 'desc' | null) => void; /** Callback when metrics are reordered via drag/drop */ onReorder?: (fromIndex: number, toIndex: number) => void; /** Callback when a metric is dragged to the filter section */ onDragToFilter?: (field: string) => void; } /** * Props for the BreakdownSection component */ export interface BreakdownSectionProps { /** Selected breakdowns */ breakdowns: BreakdownItem[]; /** Schema for resolving field titles */ schema: MetaResponse | null; /** Add breakdown handler */ onAdd: () => void; /** Remove breakdown handler */ onRemove: (id: string) => void; /** Change granularity for time dimension */ onGranularityChange: (id: string, granularity: string) => void; /** Toggle comparison for time dimension */ onComparisonToggle?: (id: string) => void; /** Whether the section is expanded */ isExpanded?: boolean; /** Toggle expansion */ onToggleExpanded?: () => void; /** Current sort order */ order?: Record<string, 'asc' | 'desc'>; /** Callback when sort order changes */ onOrderChange?: (fieldName: string, direction: 'asc' | 'desc' | null) => void; /** Callback when breakdowns are reordered via drag/drop */ onReorder?: (fromIndex: number, toIndex: number) => void; /** Callback when a breakdown is dragged to the filter section */ onDragToFilter?: (field: string) => void; } /** * Props for MetricItemCard component */ export interface MetricItemCardProps { /** Metric item data */ metric: MetricItem; /** Field metadata (for title, description) */ fieldMeta: MetaField | null; /** Remove handler */ onRemove: () => void; /** Current sort direction for this field */ sortDirection?: 'asc' | 'desc' | null; /** Sort priority (1, 2, 3...) if sorted */ sortPriority?: number; /** Toggle sort handler */ onToggleSort?: () => void; /** Index in the list (for drag/drop) */ index?: number; /** Whether this item is being dragged */ isDragging?: boolean; /** Whether dragging over this item */ isDragOver?: boolean; /** Drag start handler */ onDragStart?: (e: DragEvent, index: number) => void; /** Drag over handler */ onDragOver?: (e: DragEvent, index: number) => void; /** Drop handler */ onDrop?: (e: DragEvent, index: number) => void; /** Drag end handler */ onDragEnd?: () => void; } /** * Props for BreakdownItemCard component */ export interface BreakdownItemCardProps { /** Breakdown item data */ breakdown: BreakdownItem; /** Field metadata (for title, description) */ fieldMeta: MetaField | null; /** Remove handler */ onRemove: () => void; /** Granularity change handler (for time dimensions) */ onGranularityChange?: (granularity: string) => void; /** Toggle comparison for time dimensions */ onComparisonToggle?: () => void; /** Whether another time dimension already has comparison enabled */ comparisonDisabled?: boolean; /** Current sort direction for this field */ sortDirection?: 'asc' | 'desc' | null; /** Sort priority (1, 2, 3...) if sorted */ sortPriority?: number; /** Toggle sort handler */ onToggleSort?: () => void; /** Index in the list (for drag/drop) */ index?: number; /** Whether this item is being dragged */ isDragging?: boolean; /** Whether dragging over this item */ isDragOver?: boolean; /** Drag start handler */ onDragStart?: (e: DragEvent, index: number) => void; /** Drag over handler */ onDragOver?: (e: DragEvent, index: number) => void; /** Drop handler */ onDrop?: (e: DragEvent, index: number) => void; /** Drag end handler */ onDragEnd?: () => void; } /** * Props for the main AnalysisBuilder component */ export interface AnalysisBuilderProps { /** Additional CSS classes */ className?: string; /** Maximum height for the component (e.g., '800px', '100vh', 'calc(100vh - 64px)') */ maxHeight?: string; /** * Initial query configuration to load. * Accepts either a single CubeQuery or a MultiQueryConfig - the component handles both internally. * This keeps multi-query complexity contained within AnalysisBuilder. */ initialQuery?: CubeQuery | MultiQueryConfig; /** Initial chart configuration (for editing existing portlets) */ initialChartConfig?: { chartType?: ChartType; chartConfig?: ChartAxisConfig; displayConfig?: ChartDisplayConfig; }; /** Initial data to display (avoids re-fetching when editing existing portlets) */ initialData?: any[]; /** Color palette for chart visualization */ colorPalette?: ColorPalette; /** Disable localStorage persistence */ disableLocalStorage?: boolean; /** Hide settings button */ hideSettings?: boolean; /** Callback when query changes (for modal integration) */ onQueryChange?: (query: CubeQuery) => void; /** Callback when chart config changes */ onChartConfigChange?: (config: { chartType: ChartType; chartConfig: ChartAxisConfig; displayConfig: ChartDisplayConfig; }) => void; } /** * Ref interface for AnalysisBuilder (for external access) */ export interface AnalysisBuilderRef { /** * Get the current query configuration. * Returns either a CubeQuery (single query) or MultiQueryConfig (multiple queries). * Consumers should just JSON.stringify the result - no need to check the type. */ getQueryConfig: () => CubeQuery | MultiQueryConfig; /** Get current chart configuration */ getChartConfig: () => { chartType: ChartType; chartConfig: ChartAxisConfig; displayConfig: ChartDisplayConfig; }; /** Execute the current query */ executeQuery: () => void; /** Clear the current query */ clearQuery: () => void; } /** * Local storage state shape for persistence */ export interface AnalysisBuilderStorageState { metrics: MetricItem[]; breakdowns: BreakdownItem[]; filters: Filter[]; order?: Record<string, 'asc' | 'desc'>; chartType: ChartType; chartConfig: ChartAxisConfig; displayConfig: ChartDisplayConfig; activeView: 'table' | 'chart'; queryStates?: AnalysisBuilderState[]; activeQueryIndex?: number; mergeStrategy?: 'concat' | 'merge'; /** Dimension keys used for merging in 'merge' strategy */ mergeKeys?: string[]; } /** * Recent fields storage shape */ export interface RecentFieldsStorage { metrics: string[]; breakdowns: string[]; } /** * Time granularity options */ export declare const TIME_GRANULARITIES: readonly [{ readonly value: "hour"; readonly label: "Hour"; }, { readonly value: "day"; readonly label: "Day"; }, { readonly value: "week"; readonly label: "Week"; }, { readonly value: "month"; readonly label: "Month"; }, { readonly value: "quarter"; readonly label: "Quarter"; }, { readonly value: "year"; readonly label: "Year"; }]; export type TimeGranularity = typeof TIME_GRANULARITIES[number]['value'];