cliftonc / dashboard
Install for your project team
Run this command in your project directory to install the skill for your entire team:
mkdir -p .claude/skills/dashboard && curl -o .claude/skills/dashboard/SKILL.md https://fastmcp.me/Skills/DownloadRaw?id=308
Project Skills
This skill will be saved in .claude/skills/dashboard/ and checked into git. All team members will have access to it automatically.
Important: Please verify the skill by reviewing its instructions before using it.
Create interactive analytics dashboards with React components from drizzle-cube/client. Use when building dashboards, configuring portlets, setting up grid layouts, or creating analytics UIs with drizzle-cube React components.
1 views
0 installs
Skill Content
---
name: dashboard
description: Create interactive analytics dashboards with React components from drizzle-cube/client. Use when building dashboards, configuring portlets, setting up grid layouts, or creating analytics UIs with drizzle-cube React components.
---
# Drizzle Cube Dashboard
This skill helps you create interactive analytics dashboards using Drizzle Cube's React components. Build complete dashboards with charts, KPIs, and data tables in a responsive grid layout.
## Core Concept
A Drizzle Cube dashboard consists of:
- **CubeProvider** - Context provider for API connection
- **AnalyticsDashboard** - Main dashboard container
- **Portlets** - Individual widgets (charts, KPIs, tables)
- **Grid Layout** - Responsive positioning system
## Installation
```bash
npm install drizzle-cube react react-dom
```
## Basic Dashboard Setup
### 1. Wrap App with CubeProvider
```typescript
import { CubeProvider } from 'drizzle-cube/client'
function App() {
return (
<CubeProvider
apiOptions={{ apiUrl: '/cubejs-api/v1' }}
token="your-auth-token"
>
<YourDashboard />
</CubeProvider>
)
}
```
### 2. Create Dashboard Component
```typescript
import { AnalyticsDashboard } from 'drizzle-cube/client'
import { useState } from 'react'
function YourDashboard() {
const [config, setConfig] = useState({
portlets: [
{
id: 'portlet-1',
title: 'Employee Count by Department',
query: JSON.stringify({
measures: ['Employees.count'],
dimensions: ['Departments.name']
}),
chartType: 'bar',
chartConfig: {
xAxis: ['Departments.name'],
yAxis: ['Employees.count']
},
x: 0,
y: 0,
w: 6,
h: 4
}
]
})
return (
<AnalyticsDashboard
config={config}
editable={true}
onConfigChange={setConfig}
onSave={async (newConfig) => {
// Save to backend
await saveDashboard(newConfig)
}}
/>
)
}
```
## CubeProvider Configuration
### Basic Configuration
```typescript
<CubeProvider
apiOptions={{ apiUrl: '/cubejs-api/v1' }}
token="auth-token"
features={{ enableAI: true }} // Optional: Enable AI features
>
{children}
</CubeProvider>
```
**Props:**
- `apiOptions`: Object with `apiUrl` for Cube API endpoint
- `token`: Authentication token (optional)
- `features`: Optional features configuration (e.g., `{ enableAI: true }`)
### With Dynamic Token
```typescript
import { useState, useEffect } from 'react'
import { CubeProvider } from 'drizzle-cube/client'
function App() {
const [token, setToken] = useState(null)
useEffect(() => {
// Fetch token from auth system
const fetchToken = async () => {
const authToken = await getAuthToken()
setToken(authToken)
}
fetchToken()
}, [])
if (!token) return <div>Loading...</div>
return (
<CubeProvider
apiOptions={{ apiUrl: '/cubejs-api/v1' }}
token={token}
>
<Dashboard />
</CubeProvider>
)
}
```
### With Runtime Configuration Updates
```typescript
import { useCubeContext } from 'drizzle-cube/client'
function DashboardSettings() {
const { updateApiConfig } = useCubeContext()
const switchEnvironment = (env) => {
const apiUrl = env === 'prod'
? '/api/cubejs-api/v1'
: '/dev-api/cubejs-api/v1'
const token = getTokenForEnvironment(env)
updateApiConfig({ apiUrl }, token)
}
return (
<div>
<button onClick={() => switchEnvironment('dev')}>Dev</button>
<button onClick={() => switchEnvironment('prod')}>Prod</button>
</div>
)
}
```
## Dashboard Configuration
### Dashboard Config Structure
```typescript
interface DashboardConfig {
portlets: PortletConfig[]
layouts?: { [key: string]: any } // Optional react-grid-layout layouts
colorPalette?: string // Optional color palette name (not limited to specific values)
}
interface PortletConfig {
id: string // Unique identifier
title: string // Display title
query: string // JSON string of CubeQuery
chartType: ChartType // Chart type
chartConfig?: ChartAxisConfig // Axis configuration
displayConfig?: ChartDisplayConfig // Visual settings
x: number // Grid X position (0-based)
y: number // Grid Y position (0-based)
w: number // Grid width (columns)
h: number // Grid height (rows)
}
```
### Complete Dashboard Example
```typescript
import { AnalyticsDashboard } from 'drizzle-cube/client'
import { useState } from 'react'
function EmployeeDashboard() {
const [config, setConfig] = useState({
colorPalette: 'ocean',
portlets: [
// KPI - Total Employees
{
id: 'kpi-total',
title: 'Total Employees',
query: JSON.stringify({
measures: ['Employees.count']
}),
chartType: 'kpiNumber',
displayConfig: {
prefix: '',
suffix: ' employees',
decimals: 0
},
x: 0,
y: 0,
w: 3,
h: 2
},
// KPI - Average Salary
{
id: 'kpi-salary',
title: 'Average Salary',
query: JSON.stringify({
measures: ['Employees.avgSalary']
}),
chartType: 'kpiNumber',
displayConfig: {
prefix: '$',
suffix: '',
decimals: 2
},
x: 3,
y: 0,
w: 3,
h: 2
},
// Bar Chart - Department Distribution
{
id: 'chart-departments',
title: 'Employees by Department',
query: JSON.stringify({
measures: ['Employees.count'],
dimensions: ['Departments.name'],
order: { 'Employees.count': 'desc' }
}),
chartType: 'bar',
chartConfig: {
xAxis: ['Departments.name'],
yAxis: ['Employees.count']
},
displayConfig: {
showLegend: true,
orientation: 'vertical'
},
x: 0,
y: 2,
w: 6,
h: 4
},
// Line Chart - Hiring Trend
{
id: 'chart-hiring',
title: 'Hiring Trend',
query: JSON.stringify({
measures: ['Employees.count'],
timeDimensions: [{
dimension: 'Employees.createdAt',
granularity: 'month',
dateRange: 'last 12 months'
}]
}),
chartType: 'line',
chartConfig: {
xAxis: ['Employees.createdAt'],
yAxis: ['Employees.count']
},
displayConfig: {
showGrid: true,
showTooltip: true
},
x: 6,
y: 0,
w: 6,
h: 6
},
// Table - Employee List
{
id: 'table-employees',
title: 'Recent Hires',
query: JSON.stringify({
dimensions: [
'Employees.name',
'Employees.email',
'Departments.name',
'Employees.createdAt'
],
order: { 'Employees.createdAt': 'desc' },
limit: 10
}),
chartType: 'table',
x: 0,
y: 6,
w: 12,
h: 4
}
]
})
const handleSave = async (newConfig) => {
try {
await fetch('/api/dashboards/employees', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newConfig)
})
alert('Dashboard saved!')
} catch (error) {
alert('Failed to save dashboard')
}
}
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">Employee Analytics</h1>
<AnalyticsDashboard
config={config}
editable={true}
onConfigChange={setConfig}
onSave={handleSave}
/>
</div>
)
}
```
## Grid Layout System
The dashboard uses a 12-column responsive grid system.
### Grid Dimensions
- **Columns**: 12 columns total width
- **Rows**: Auto-height based on content
- **Units**: Each `w` = 1 column, each `h` = 60px
### Positioning Examples
```typescript
// Full width portlet
{
x: 0, // Start at left
y: 0, // Start at top
w: 12, // Full width (12 columns)
h: 4 // 320px height (h × 80px rowHeight)
}
// Half width portlets side-by-side
[
{
x: 0, // Left half
y: 0,
w: 6, // Half width (6 columns)
h: 4
},
{
x: 6, // Right half
y: 0,
w: 6, // Half width
h: 4
}
]
// Three equal columns
[
{ x: 0, y: 0, w: 4, h: 3 }, // Left third
{ x: 4, y: 0, w: 4, h: 3 }, // Middle third
{ x: 8, y: 0, w: 4, h: 3 } // Right third
]
// Dashboard header row + main content
[
{ x: 0, y: 0, w: 3, h: 2 }, // KPI 1
{ x: 3, y: 0, w: 3, h: 2 }, // KPI 2
{ x: 6, y: 0, w: 3, h: 2 }, // KPI 3
{ x: 9, y: 0, w: 3, h: 2 }, // KPI 4
{ x: 0, y: 2, w: 12, h: 6 } // Full-width chart below
]
```
### Responsive Breakpoints
The grid automatically adapts to screen sizes:
- **Large (≥1200px)**: 12 columns
- **Medium (≥996px)**: 10 columns
- **Small (≥768px)**: 6 columns
- **XSmall (≥480px)**: 4 columns
- **XXSmall (<480px)**: 2 columns
## Editable vs Read-Only
### Editable Dashboard
```typescript
<AnalyticsDashboard
config={config}
editable={true} // Enable editing
onConfigChange={setConfig} // Handle layout changes
onSave={handleSave} // Save button handler
/>
```
Features in edit mode:
- Drag and drop portlets
- Resize portlets
- Edit chart configurations
- Add/remove portlets
- Save button appears
### Read-Only Dashboard
```typescript
<AnalyticsDashboard
config={config}
editable={false} // Disable editing
/>
```
Features disabled:
- No drag and drop
- No resize handles
- No edit buttons
- No save button
- View-only mode
## Persisting Dashboard Configuration
### Save to Backend
```typescript
function Dashboard() {
const [config, setConfig] = useState(null)
const [isDirty, setIsDirty] = useState(false)
// Load dashboard on mount
useEffect(() => {
const loadDashboard = async () => {
const response = await fetch('/api/dashboards/my-dashboard')
const data = await response.json()
setConfig(data)
}
loadDashboard()
}, [])
const handleSave = async (newConfig) => {
try {
await fetch('/api/dashboards/my-dashboard', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newConfig)
})
setIsDirty(false)
console.log('Dashboard saved successfully')
} catch (error) {
console.error('Failed to save dashboard:', error)
throw error // AnalyticsDashboard will handle error display
}
}
if (!config) return <div>Loading...</div>
return (
<AnalyticsDashboard
config={config}
editable={true}
onConfigChange={setConfig}
onSave={handleSave}
onDirtyStateChange={setIsDirty}
/>
)
}
```
### Local Storage Persistence
```typescript
function Dashboard() {
const [config, setConfig] = useState(() => {
// Load from localStorage on mount
const saved = localStorage.getItem('dashboard-config')
return saved ? JSON.parse(saved) : defaultConfig
})
const handleSave = async (newConfig) => {
// Save to localStorage
localStorage.setItem('dashboard-config', JSON.stringify(newConfig))
}
const handleConfigChange = (newConfig) => {
setConfig(newConfig)
// Auto-save to localStorage
localStorage.setItem('dashboard-config', JSON.stringify(newConfig))
}
return (
<AnalyticsDashboard
config={config}
editable={true}
onConfigChange={handleConfigChange}
onSave={handleSave}
/>
)
}
```
## Using Dashboard Hooks
### useCubeQuery Hook
Execute queries programmatically:
```typescript
import { useCubeQuery } from 'drizzle-cube/client'
function CustomWidget() {
const { resultSet, isLoading, error } = useCubeQuery({
measures: ['Employees.count'],
dimensions: ['Departments.name']
})
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<div>
{resultSet.tablePivot().map(row => (
<div key={row['Departments.name']}>
{row['Departments.name']}: {row['Employees.count']}
</div>
))}
</div>
)
}
```
### useCubeMeta Hook
Access cube metadata:
```typescript
import { useCubeMeta } from 'drizzle-cube/client'
function MetadataExplorer() {
const { meta, loading, error } = useCubeMeta()
if (loading) return <div>Loading metadata...</div>
if (error) return <div>Error loading metadata</div>
return (
<div>
<h2>Available Cubes</h2>
{meta.cubes.map(cube => (
<div key={cube.name}>
<h3>{cube.title || cube.name}</h3>
<p>Measures: {cube.measures.length}</p>
<p>Dimensions: {cube.dimensions.length}</p>
</div>
))}
</div>
)
}
```
### useCubeContext Hook
Access provider context:
```typescript
import { useCubeContext } from 'drizzle-cube/client'
function ApiSettings() {
const {
cubeApi,
meta,
metaLoading,
updateApiConfig,
refetchMeta
} = useCubeContext()
const switchServer = () => {
updateApiConfig(
{ apiUrl: '/new-api/cubejs-api/v1' },
'new-token'
)
refetchMeta() // Reload metadata from new server
}
return (
<div>
<p>API URL: {cubeApi.apiUrl}</p>
<p>Cubes loaded: {meta?.cubes.length || 0}</p>
<button onClick={switchServer}>Switch Server</button>
<button onClick={refetchMeta}>Refresh Metadata</button>
</div>
)
}
```
## Color Palettes
Built-in color palettes for consistent theming:
```typescript
// Available palettes
const palettes = [
'default', // Blue-green gradient
'ocean', // Blue tones
'sunset', // Orange-red gradient
'forest' // Green tones
]
// Usage
<AnalyticsDashboard
config={{
...config,
colorPalette: 'ocean'
}}
/>
```
## Complete Application Example
```typescript
// App.tsx
import { CubeProvider, AnalyticsDashboard } from 'drizzle-cube/client'
import { useState, useEffect } from 'react'
function App() {
const [token, setToken] = useState(null)
const [config, setConfig] = useState(null)
useEffect(() => {
// Initialize auth and load dashboard
const initialize = async () => {
const authToken = await getAuthToken()
setToken(authToken)
const dashboard = await fetch('/api/dashboards/main', {
headers: { Authorization: `Bearer ${authToken}` }
}).then(r => r.json())
setConfig(dashboard)
}
initialize()
}, [])
const handleSave = async (newConfig) => {
await fetch('/api/dashboards/main', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(newConfig)
})
}
if (!token || !config) {
return <div>Loading...</div>
}
return (
<CubeProvider
apiOptions={{ apiUrl: '/cubejs-api/v1' }}
token={token}
>
<div className="min-h-screen bg-gray-100">
<header className="bg-white shadow px-4 py-6">
<h1 className="text-3xl font-bold">Analytics Dashboard</h1>
</header>
<main className="container mx-auto p-4">
<AnalyticsDashboard
config={config}
editable={true}
onConfigChange={setConfig}
onSave={handleSave}
/>
</main>
</div>
</CubeProvider>
)
}
export default App
```
## Programmatic Dashboard Filters
Dashboard filters can be applied programmatically by passing them through the `dashboardFilters` prop. This is useful for embedding dashboards with pre-configured filters from your application.
### Filter Structure
```typescript
interface DashboardFilter {
id: string // Unique identifier
label: string // Display label
filter: Filter // Filter definition (SimpleFilter or GroupFilter)
}
// Simple filter
interface SimpleFilter {
member: string // Field name (e.g., 'Employees.department')
operator: FilterOperator // equals, notEquals, contains, gt, lt, etc.
values: any[] // Filter values
}
// Group filter (AND/OR logic)
interface GroupFilter {
type: 'and' | 'or'
filters: Filter[] // Array of SimpleFilter or GroupFilter
}
```
### Basic Programmatic Filter Example
```typescript
import { AnalyticsDashboard } from 'drizzle-cube/client'
import { useState } from 'react'
function DashboardWithFilters() {
const [config, setConfig] = useState({
portlets: [
{
id: 'portlet-1',
title: 'Employee Count',
query: JSON.stringify({
measures: ['Employees.count'],
dimensions: ['Departments.name']
}),
chartType: 'bar',
chartConfig: {
xAxis: ['Departments.name'],
yAxis: ['Employees.count']
},
// Map which dashboard filters apply to this portlet
dashboardFilterMapping: ['filter-1', 'filter-2'],
x: 0,
y: 0,
w: 12,
h: 4
}
]
})
// Define programmatic filters
const dashboardFilters = [
{
id: 'filter-1',
label: 'Active Employees Only',
filter: {
member: 'Employees.isActive',
operator: 'equals',
values: [true]
}
},
{
id: 'filter-2',
label: 'Engineering Department',
filter: {
member: 'Departments.name',
operator: 'equals',
values: ['Engineering']
}
}
]
return (
<AnalyticsDashboard
config={config}
dashboardFilters={dashboardFilters} // Pass programmatic filters
editable={false}
onConfigChange={setConfig}
/>
)
}
```
### Dynamic Filters from URL Parameters
```typescript
import { AnalyticsDashboard } from 'drizzle-cube/client'
import { useSearchParams } from 'react-router-dom'
import { useMemo } from 'react'
function DashboardWithURLFilters() {
const [searchParams] = useSearchParams()
const [config, setConfig] = useState(dashboardConfig)
// Build filters from URL parameters
const dashboardFilters = useMemo(() => {
const filters = []
// Department filter from ?department=Engineering
const department = searchParams.get('department')
if (department) {
filters.push({
id: 'url-department',
label: `Department: ${department}`,
filter: {
member: 'Departments.name',
operator: 'equals',
values: [department]
}
})
}
// Date range filter from ?startDate=2024-01-01&endDate=2024-12-31
const startDate = searchParams.get('startDate')
const endDate = searchParams.get('endDate')
if (startDate && endDate) {
filters.push({
id: 'url-daterange',
label: 'Custom Date Range',
filter: {
member: 'Employees.createdAt',
operator: 'inDateRange',
values: [startDate, endDate]
}
})
}
// Status filter from ?status=active
const status = searchParams.get('status')
if (status === 'active') {
filters.push({
id: 'url-status',
label: 'Active Only',
filter: {
member: 'Employees.isActive',
operator: 'equals',
values: [true]
}
})
}
return filters
}, [searchParams])
return (
<AnalyticsDashboard
config={config}
dashboardFilters={dashboardFilters}
editable={false}
onConfigChange={setConfig}
/>
)
}
```
### User-Based Filters
```typescript
import { AnalyticsDashboard } from 'drizzle-cube/client'
import { useAuth } from './auth'
function UserDashboard() {
const { user } = useAuth()
const [config, setConfig] = useState(dashboardConfig)
// Apply filters based on user role/permissions
const dashboardFilters = useMemo(() => {
const filters = []
// Department managers only see their department
if (user.role === 'department_manager') {
filters.push({
id: 'user-department',
label: `Your Department: ${user.department}`,
filter: {
member: 'Departments.name',
operator: 'equals',
values: [user.department]
}
})
}
// Regional managers see their region
if (user.role === 'regional_manager') {
filters.push({
id: 'user-region',
label: `Your Region: ${user.region}`,
filter: {
member: 'Employees.region',
operator: 'equals',
values: [user.region]
}
})
}
// All users see only active employees by default
filters.push({
id: 'default-active',
label: 'Active Employees',
filter: {
member: 'Employees.isActive',
operator: 'equals',
values: [true]
}
})
return filters
}, [user])
return (
<AnalyticsDashboard
config={config}
dashboardFilters={dashboardFilters}
editable={false}
onConfigChange={setConfig}
/>
)
}
```
### Complex Group Filters
```typescript
function DashboardWithComplexFilters() {
const [config, setConfig] = useState(dashboardConfig)
const dashboardFilters = [
{
id: 'complex-filter',
label: 'Engineering or High Salary',
filter: {
type: 'or',
filters: [
{
member: 'Departments.name',
operator: 'equals',
values: ['Engineering']
},
{
member: 'Employees.salary',
operator: 'gte',
values: [100000]
}
]
}
},
{
id: 'date-and-status',
label: 'Recent Active Hires',
filter: {
type: 'and',
filters: [
{
member: 'Employees.isActive',
operator: 'equals',
values: [true]
},
{
member: 'Employees.createdAt',
operator: 'afterDate',
values: ['2024-01-01']
}
]
}
}
]
return (
<AnalyticsDashboard
config={config}
dashboardFilters={dashboardFilters}
editable={false}
onConfigChange={setConfig}
/>
)
}
```
### Mapping Filters to Specific Portlets
By default, programmatic filters are NOT applied to any portlets. You must explicitly map which filters apply to which portlets using the `dashboardFilterMapping` array:
```typescript
const config = {
portlets: [
{
id: 'portlet-1',
title: 'All Employees',
query: JSON.stringify({
measures: ['Employees.count']
}),
chartType: 'kpiNumber',
// This portlet uses both filters
dashboardFilterMapping: ['filter-active', 'filter-department'],
x: 0,
y: 0,
w: 4,
h: 2
},
{
id: 'portlet-2',
title: 'All Departments (Unfiltered)',
query: JSON.stringify({
measures: ['Departments.count']
}),
chartType: 'kpiNumber',
// This portlet uses NO filters (empty array or omit property)
dashboardFilterMapping: [],
x: 4,
y: 0,
w: 4,
h: 2
},
{
id: 'portlet-3',
title: 'Active Employees Only',
query: JSON.stringify({
measures: ['Employees.count']
}),
chartType: 'kpiNumber',
// This portlet uses only the active filter
dashboardFilterMapping: ['filter-active'],
x: 8,
y: 0,
w: 4,
h: 2
}
]
}
const dashboardFilters = [
{
id: 'filter-active',
label: 'Active Only',
filter: {
member: 'Employees.isActive',
operator: 'equals',
values: [true]
}
},
{
id: 'filter-department',
label: 'Engineering',
filter: {
member: 'Departments.name',
operator: 'equals',
values: ['Engineering']
}
}
]
```
### Available Filter Operators
```typescript
// String operators
'equals' // Exact match
'notEquals' // Not equal
'contains' // Contains substring
'notContains' // Does not contain substring
'startsWith' // Starts with
'endsWith' // Ends with
// Numeric operators
'gt' // Greater than
'gte' // Greater than or equal
'lt' // Less than
'lte' // Less than or equal
// Null operators
'set' // Is not null
'notSet' // Is null
// Date operators
'inDateRange' // Between two dates (values: [startDate, endDate])
'beforeDate' // Before a date (values: [date])
'afterDate' // After a date (values: [date])
```
### Filter Merging Behavior
Filters are merged with portlet queries using AND logic:
```typescript
// Portlet query
{
measures: ['Employees.count'],
filters: [{
member: 'Employees.salary',
operator: 'gte',
values: [50000]
}]
}
// Dashboard filter applied
{
member: 'Employees.isActive',
operator: 'equals',
values: [true]
}
// Final merged query (automatic)
{
measures: ['Employees.count'],
filters: {
and: [
{
member: 'Employees.isActive',
operator: 'equals',
values: [true]
},
{
member: 'Employees.salary',
operator: 'gte',
values: [50000]
}
]
}
}
```
### Hiding Filter UI in Edit Mode
Programmatic filters are always hidden from the filter panel UI. They are applied silently in the background and cannot be edited by users through the dashboard interface. This is useful for:
- Security filtering (user can only see their data)
- Environment filtering (production vs staging)
- Application-level filtering (tenant isolation)
- URL parameter filtering (dashboard embedding)
### Complete Example: Multi-Tenant Dashboard
```typescript
import { AnalyticsDashboard } from 'drizzle-cube/client'
import { useAuth } from './auth'
import { useSearchParams } from 'react-router-dom'
function TenantDashboard() {
const { user } = useAuth()
const [searchParams] = useSearchParams()
const [config, setConfig] = useState(dashboardConfig)
// Combine tenant isolation + URL filters
const dashboardFilters = useMemo(() => {
const filters = []
// REQUIRED: Tenant isolation (security)
filters.push({
id: 'tenant-isolation',
label: `Organization: ${user.organizationName}`,
filter: {
member: 'Employees.organisationId',
operator: 'equals',
values: [user.organizationId]
}
})
// OPTIONAL: URL department filter
const department = searchParams.get('department')
if (department) {
filters.push({
id: 'url-department',
label: `Department: ${department}`,
filter: {
member: 'Departments.name',
operator: 'equals',
values: [department]
}
})
}
// OPTIONAL: URL date range
const dateRange = searchParams.get('range')
if (dateRange === 'last30days') {
filters.push({
id: 'url-daterange',
label: 'Last 30 Days',
filter: {
member: 'Employees.createdAt',
operator: 'inDateRange',
values: ['2024-01-01', '2024-01-31']
}
})
}
return filters
}, [user, searchParams])
return (
<AnalyticsDashboard
config={config}
dashboardFilters={dashboardFilters}
editable={user.canEditDashboards}
onConfigChange={setConfig}
/>
)
}
```
## Best Practices
1. **Wrap with CubeProvider** - All dashboard components need CubeProvider
2. **Persist configuration** - Save dashboard config to backend/localStorage
3. **Handle errors gracefully** - Show user-friendly error messages
4. **Use appropriate grid sizes** - Consider mobile responsiveness
5. **Limit portlet count** - Too many portlets impact performance
6. **Cache query results** - Use React Query or similar for caching
7. **Validate queries** - Ensure query strings are valid JSON
8. **Use programmatic filters for security** - Apply tenant/user isolation via dashboardFilters
9. **Map filters explicitly** - Use dashboardFilterMapping to control which portlets get which filters
## Common Pitfalls
- **Missing CubeProvider** - Dashboard components require provider context
- **Invalid query JSON** - Query must be valid JSON string
- **Grid overlaps** - Ensure portlet positions don't overlap
- **Missing security token** - API calls require authentication
- **Large initial queries** - Use filters to limit initial data load
## Next Steps
- Configure **chart types** with the specific chart skills
- Build **queries** with the `queries` skill
- Set up **server APIs** with the `server-setup` skill
- Learn about **cube definitions** with the `cube-definition` skill