mirror of https://github.com/buster-so/buster.git
Adding Enum drop downs and the ability to hide metric level filters
This commit is contained in:
parent
50290f4d2c
commit
95a5178eb6
|
@ -6,6 +6,8 @@ import { cn } from '@/lib/utils';
|
|||
interface DashboardFiltersProps {
|
||||
commonFilters: MetricFilter[];
|
||||
onFilterValuesChange: (filterValues: Record<string, unknown>) => void;
|
||||
showMetricFilters: boolean;
|
||||
onToggleMetricFilters: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
|
@ -18,30 +20,50 @@ function canUseOperator(filter: MetricFilter): boolean {
|
|||
return MODES_SUPPORTING_OPERATORS.has(filter.mode);
|
||||
}
|
||||
|
||||
const CUSTOM_VALUE_KEY = '__custom__';
|
||||
|
||||
export const DashboardFilters: React.FC<DashboardFiltersProps> = ({
|
||||
commonFilters,
|
||||
onFilterValuesChange,
|
||||
showMetricFilters,
|
||||
onToggleMetricFilters,
|
||||
className,
|
||||
}) => {
|
||||
const [filterValues, setFilterValues] = useState<Record<string, string>>({});
|
||||
const [filterOperators, setFilterOperators] = useState<Record<string, string>>({});
|
||||
const [customMode, setCustomMode] = useState<Record<string, boolean>>({});
|
||||
|
||||
if (!commonFilters || commonFilters.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const hasCommonFilters = commonFilters && commonFilters.length > 0;
|
||||
|
||||
const handleInputChange = (key: string, value: string) => {
|
||||
setFilterValues({ ...filterValues, [key]: value });
|
||||
};
|
||||
|
||||
const handleEnumChange = (key: string, value: string) => {
|
||||
if (value === CUSTOM_VALUE_KEY) {
|
||||
setCustomMode({ ...customMode, [key]: true });
|
||||
setFilterValues({ ...filterValues, [key]: '' });
|
||||
} else {
|
||||
const newFilterValues = { ...filterValues, [key]: value };
|
||||
setCustomMode({ ...customMode, [key]: false });
|
||||
setFilterValues(newFilterValues);
|
||||
|
||||
// Immediately process the enum selection
|
||||
processFilterValues(newFilterValues, filterOperators);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOperatorChange = (key: string, op: string) => {
|
||||
setFilterOperators({ ...filterOperators, [key]: op });
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
const processFilterValues = (
|
||||
currentFilterValues: Record<string, string>,
|
||||
currentFilterOperators: Record<string, string>
|
||||
) => {
|
||||
// Convert values to appropriate types
|
||||
const typedValues: Record<string, unknown> = {};
|
||||
Object.entries(filterValues).forEach(([filterKey, filterValue]) => {
|
||||
Object.entries(currentFilterValues).forEach(([filterKey, filterValue]) => {
|
||||
const filter = commonFilters.find((f) => f.key === filterKey);
|
||||
if (!filter || !filterValue) return;
|
||||
|
||||
|
@ -63,7 +85,7 @@ export const DashboardFilters: React.FC<DashboardFiltersProps> = ({
|
|||
}
|
||||
|
||||
// Check if we should include operator override
|
||||
const operator = filterOperators[filterKey];
|
||||
const operator = currentFilterOperators[filterKey];
|
||||
const shouldIncludeOperator = operator && canUseOperator(filter);
|
||||
|
||||
if (shouldIncludeOperator) {
|
||||
|
@ -76,39 +98,78 @@ export const DashboardFilters: React.FC<DashboardFiltersProps> = ({
|
|||
onFilterValuesChange(typedValues);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
processFilterValues(filterValues, filterOperators);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('bg-muted/50 border-border flex flex-wrap gap-3 border-b p-4', className)}>
|
||||
<div className="text-sm font-semibold mr-2">Dashboard Filters:</div>
|
||||
{commonFilters.map((filter) => (
|
||||
<div key={filter.key} className="flex items-center gap-2">
|
||||
<label htmlFor={`dashboard-${filter.key}`} className="text-muted-foreground text-sm font-medium">
|
||||
{filter.key}:
|
||||
</label>
|
||||
{canUseOperator(filter) && (
|
||||
<select
|
||||
value={filterOperators[filter.key] || filter.op || '='}
|
||||
onChange={(e) => handleOperatorChange(filter.key, e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
className="h-8 rounded-md border border-input bg-background px-2 text-sm"
|
||||
>
|
||||
{COMPARISON_OPERATORS.map((op) => (
|
||||
<option key={op} value={op}>
|
||||
{op}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<Input
|
||||
id={`dashboard-${filter.key}`}
|
||||
type="text"
|
||||
value={filterValues[filter.key] || ''}
|
||||
onChange={(e) => handleInputChange(filter.key, e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
placeholder={getPlaceholder(filter)}
|
||||
className="h-8 w-48"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className={cn('bg-muted/50 border-border flex flex-wrap items-center gap-3 border-b p-4', className)}>
|
||||
{hasCommonFilters && (
|
||||
<>
|
||||
<div className="text-sm font-semibold mr-2">Dashboard Filters:</div>
|
||||
{commonFilters.map((filter) => {
|
||||
const hasEnum = filter.validate?.enum && filter.validate.enum.length > 0;
|
||||
const isCustomMode = customMode[filter.key];
|
||||
|
||||
return (
|
||||
<div key={filter.key} className="flex items-center gap-2">
|
||||
<label htmlFor={`dashboard-${filter.key}`} className="text-muted-foreground text-sm font-medium">
|
||||
{filter.key}:
|
||||
</label>
|
||||
{canUseOperator(filter) && (
|
||||
<select
|
||||
value={filterOperators[filter.key] || filter.op || '='}
|
||||
onChange={(e) => handleOperatorChange(filter.key, e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
className="h-8 rounded-md border border-input bg-background px-2 text-sm"
|
||||
>
|
||||
{COMPARISON_OPERATORS.map((op) => (
|
||||
<option key={op} value={op}>
|
||||
{op}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
{hasEnum && !isCustomMode ? (
|
||||
<select
|
||||
id={`dashboard-${filter.key}`}
|
||||
value={filterValues[filter.key] || ''}
|
||||
onChange={(e) => handleEnumChange(filter.key, e.target.value)}
|
||||
className="h-8 w-48 rounded-md border border-input bg-background px-2 text-sm"
|
||||
>
|
||||
<option value="">Select value...</option>
|
||||
{filter.validate!.enum!.map((enumValue) => (
|
||||
<option key={String(enumValue)} value={String(enumValue)}>
|
||||
{String(enumValue)}
|
||||
</option>
|
||||
))}
|
||||
<option value={CUSTOM_VALUE_KEY}>Custom...</option>
|
||||
</select>
|
||||
) : (
|
||||
<Input
|
||||
id={`dashboard-${filter.key}`}
|
||||
type="text"
|
||||
value={filterValues[filter.key] || ''}
|
||||
onChange={(e) => handleInputChange(filter.key, e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
placeholder={getPlaceholder(filter)}
|
||||
className="h-8 w-48"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
onClick={onToggleMetricFilters}
|
||||
className={cn(
|
||||
'rounded-md border border-input bg-background px-3 py-1.5 text-sm font-medium hover:bg-accent',
|
||||
hasCommonFilters && 'ml-auto'
|
||||
)}
|
||||
>
|
||||
{showMetricFilters ? 'Hide' : 'Show'} Metric Filters
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -70,13 +70,16 @@ export const MetricChartCard = React.memo(
|
|||
ref
|
||||
) => {
|
||||
const [metricSpecificFilterValues, setMetricSpecificFilterValues] = useState<Record<string, unknown>>({});
|
||||
const { dashboardFilterValues } = useDashboardFilterValues();
|
||||
const { dashboardFilterValues, showMetricFilters, isOnDashboard } = useDashboardFilterValues();
|
||||
|
||||
// Merge dashboard-level filters with metric-specific filters
|
||||
const filterValues = useMemo(() => {
|
||||
return { ...dashboardFilterValues, ...metricSpecificFilterValues };
|
||||
}, [dashboardFilterValues, metricSpecificFilterValues]);
|
||||
|
||||
// Show metric filters if: standalone metric OR (on dashboard AND toggle is on)
|
||||
const shouldShowMetricFilters = !isOnDashboard || showMetricFilters;
|
||||
|
||||
const { data: metric, isFetched: isFetchedMetric } = useGetMetric(
|
||||
{ id: metricId, versionNumber },
|
||||
{ select: stableMetricSelect, enabled: true }
|
||||
|
@ -130,7 +133,9 @@ export const MetricChartCard = React.memo(
|
|||
metricVersionNumber={versionNumber}
|
||||
/>
|
||||
<div className={'border-border border-b'} />
|
||||
<MetricFilters filters={metric?.filters} onFilterValuesChange={setMetricSpecificFilterValues} />
|
||||
{shouldShowMetricFilters && (
|
||||
<MetricFilters filters={metric?.filters} onFilterValuesChange={setMetricSpecificFilterValues} />
|
||||
)}
|
||||
{renderChartContent && (
|
||||
<MetricViewChartContent
|
||||
chartConfig={memoizedChartConfig}
|
||||
|
|
|
@ -18,6 +18,8 @@ function canUseOperator(filter: MetricFilter): boolean {
|
|||
return MODES_SUPPORTING_OPERATORS.has(filter.mode);
|
||||
}
|
||||
|
||||
const CUSTOM_VALUE_KEY = '__custom__';
|
||||
|
||||
export const MetricFilters: React.FC<MetricFiltersProps> = ({
|
||||
filters,
|
||||
onFilterValuesChange,
|
||||
|
@ -25,6 +27,7 @@ export const MetricFilters: React.FC<MetricFiltersProps> = ({
|
|||
}) => {
|
||||
const [filterValues, setFilterValues] = useState<Record<string, string>>({});
|
||||
const [filterOperators, setFilterOperators] = useState<Record<string, string>>({});
|
||||
const [customMode, setCustomMode] = useState<Record<string, boolean>>({});
|
||||
|
||||
if (!filters || filters.length === 0) {
|
||||
return null;
|
||||
|
@ -34,14 +37,31 @@ export const MetricFilters: React.FC<MetricFiltersProps> = ({
|
|||
setFilterValues({ ...filterValues, [key]: value });
|
||||
};
|
||||
|
||||
const handleEnumChange = (key: string, value: string) => {
|
||||
if (value === CUSTOM_VALUE_KEY) {
|
||||
setCustomMode({ ...customMode, [key]: true });
|
||||
setFilterValues({ ...filterValues, [key]: '' });
|
||||
} else {
|
||||
const newFilterValues = { ...filterValues, [key]: value };
|
||||
setCustomMode({ ...customMode, [key]: false });
|
||||
setFilterValues(newFilterValues);
|
||||
|
||||
// Immediately process the enum selection
|
||||
processFilterValues(newFilterValues, filterOperators);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOperatorChange = (key: string, op: string) => {
|
||||
setFilterOperators({ ...filterOperators, [key]: op });
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
const processFilterValues = (
|
||||
currentFilterValues: Record<string, string>,
|
||||
currentFilterOperators: Record<string, string>
|
||||
) => {
|
||||
// Convert values to appropriate types
|
||||
const typedValues: Record<string, unknown> = {};
|
||||
Object.entries(filterValues).forEach(([filterKey, filterValue]) => {
|
||||
Object.entries(currentFilterValues).forEach(([filterKey, filterValue]) => {
|
||||
const filter = filters.find((f) => f.key === filterKey);
|
||||
if (!filter || !filterValue) return;
|
||||
|
||||
|
@ -63,7 +83,7 @@ export const MetricFilters: React.FC<MetricFiltersProps> = ({
|
|||
}
|
||||
|
||||
// Check if we should include operator override
|
||||
const operator = filterOperators[filterKey];
|
||||
const operator = currentFilterOperators[filterKey];
|
||||
const shouldIncludeOperator = operator && canUseOperator(filter);
|
||||
|
||||
if (shouldIncludeOperator) {
|
||||
|
@ -76,38 +96,65 @@ export const MetricFilters: React.FC<MetricFiltersProps> = ({
|
|||
onFilterValuesChange(typedValues);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
processFilterValues(filterValues, filterOperators);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('bg-muted/50 border-border flex flex-wrap gap-3 border-b p-3', className)}>
|
||||
{filters.map((filter) => (
|
||||
<div key={filter.key} className="flex items-center gap-2">
|
||||
<label htmlFor={filter.key} className="text-muted-foreground text-sm font-medium">
|
||||
{filter.key}:
|
||||
</label>
|
||||
{canUseOperator(filter) && (
|
||||
<select
|
||||
value={filterOperators[filter.key] || filter.op || '='}
|
||||
onChange={(e) => handleOperatorChange(filter.key, e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
className="h-8 rounded-md border border-input bg-background px-2 text-sm"
|
||||
>
|
||||
{COMPARISON_OPERATORS.map((op) => (
|
||||
<option key={op} value={op}>
|
||||
{op}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<Input
|
||||
id={filter.key}
|
||||
type="text"
|
||||
value={filterValues[filter.key] || ''}
|
||||
onChange={(e) => handleInputChange(filter.key, e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
placeholder={getPlaceholder(filter)}
|
||||
className="h-8 w-48"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{filters.map((filter) => {
|
||||
const hasEnum = filter.validate?.enum && filter.validate.enum.length > 0;
|
||||
const isCustomMode = customMode[filter.key];
|
||||
const showInput = !hasEnum || isCustomMode;
|
||||
|
||||
return (
|
||||
<div key={filter.key} className="flex items-center gap-2">
|
||||
<label htmlFor={filter.key} className="text-muted-foreground text-sm font-medium">
|
||||
{filter.key}:
|
||||
</label>
|
||||
{canUseOperator(filter) && (
|
||||
<select
|
||||
value={filterOperators[filter.key] || filter.op || '='}
|
||||
onChange={(e) => handleOperatorChange(filter.key, e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
className="h-8 rounded-md border border-input bg-background px-2 text-sm"
|
||||
>
|
||||
{COMPARISON_OPERATORS.map((op) => (
|
||||
<option key={op} value={op}>
|
||||
{op}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
{hasEnum && !isCustomMode ? (
|
||||
<select
|
||||
id={filter.key}
|
||||
value={filterValues[filter.key] || ''}
|
||||
onChange={(e) => handleEnumChange(filter.key, e.target.value)}
|
||||
className="h-8 w-48 rounded-md border border-input bg-background px-2 text-sm"
|
||||
>
|
||||
<option value="">Select value...</option>
|
||||
{filter.validate!.enum!.map((enumValue) => (
|
||||
<option key={String(enumValue)} value={String(enumValue)}>
|
||||
{String(enumValue)}
|
||||
</option>
|
||||
))}
|
||||
<option value={CUSTOM_VALUE_KEY}>Custom...</option>
|
||||
</select>
|
||||
) : (
|
||||
<Input
|
||||
id={filter.key}
|
||||
type="text"
|
||||
value={filterValues[filter.key] || ''}
|
||||
onChange={(e) => handleInputChange(filter.key, e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
placeholder={getPlaceholder(filter)}
|
||||
className="h-8 w-48"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -34,7 +34,7 @@ const DashboardContentControllerInner: React.FC<{
|
|||
onUpdateDashboardConfig,
|
||||
}) => {
|
||||
const [draggingId, setDraggingId] = useState<string | null>(null);
|
||||
const { setDashboardFilterValues } = useDashboardFilterValues();
|
||||
const { setDashboardFilterValues, showMetricFilters, setShowMetricFilters } = useDashboardFilterValues();
|
||||
|
||||
const commonFilters = useMemo(() => {
|
||||
const filters = getCommonFilters(metrics);
|
||||
|
@ -118,10 +118,12 @@ const DashboardContentControllerInner: React.FC<{
|
|||
<div className="dashboard-content-controller overflow-visible">
|
||||
{hasMetrics && !!dashboardRows.length && !!dashboard ? (
|
||||
<DashboardContentControllerProvider dashboard={dashboard}>
|
||||
{!readOnly && commonFilters.length > 0 && (
|
||||
{!readOnly && (
|
||||
<DashboardFilters
|
||||
commonFilters={commonFilters}
|
||||
onFilterValuesChange={setDashboardFilterValues}
|
||||
showMetricFilters={showMetricFilters}
|
||||
onToggleMetricFilters={() => setShowMetricFilters(!showMetricFilters)}
|
||||
/>
|
||||
)}
|
||||
<BusterResizeableGrid
|
||||
|
|
|
@ -3,15 +3,25 @@ import React, { createContext, useContext, useState } from 'react';
|
|||
interface DashboardFilterContextValue {
|
||||
dashboardFilterValues: Record<string, unknown>;
|
||||
setDashboardFilterValues: (values: Record<string, unknown>) => void;
|
||||
showMetricFilters: boolean;
|
||||
setShowMetricFilters: (show: boolean) => void;
|
||||
}
|
||||
|
||||
const DashboardFilterContext = createContext<DashboardFilterContextValue | undefined>(undefined);
|
||||
|
||||
export const DashboardFilterProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [dashboardFilterValues, setDashboardFilterValues] = useState<Record<string, unknown>>({});
|
||||
const [showMetricFilters, setShowMetricFilters] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<DashboardFilterContext.Provider value={{ dashboardFilterValues, setDashboardFilterValues }}>
|
||||
<DashboardFilterContext.Provider
|
||||
value={{
|
||||
dashboardFilterValues,
|
||||
setDashboardFilterValues,
|
||||
showMetricFilters,
|
||||
setShowMetricFilters
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</DashboardFilterContext.Provider>
|
||||
);
|
||||
|
@ -20,7 +30,13 @@ export const DashboardFilterProvider: React.FC<{ children: React.ReactNode }> =
|
|||
export const useDashboardFilterValues = () => {
|
||||
const context = useContext(DashboardFilterContext);
|
||||
if (!context) {
|
||||
return { dashboardFilterValues: {}, setDashboardFilterValues: () => {} };
|
||||
return {
|
||||
dashboardFilterValues: {},
|
||||
setDashboardFilterValues: () => {},
|
||||
showMetricFilters: true, // Default to true when not on dashboard
|
||||
setShowMetricFilters: () => {},
|
||||
isOnDashboard: false
|
||||
};
|
||||
}
|
||||
return context;
|
||||
return { ...context, isOnDashboard: true };
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue