A modular analytics dashboard for Gnosis metrics built with React, ECharts, YAML-driven layouts, and a cached ClickHouse-backed API.
- Connects to ClickHouse Cloud via API
- Server-side caching to minimize ClickHouse queries
- Secure handling of credentials (not exposed to frontend)
- YAML-driven dashboards/tabs/card layouts
- ECharts-based chart rendering with expand/info/download controls
- Header metric search with fuzzy matching and direct dashboard/tab navigation
- Responsive design with dark mode support
- Deployment to Vercel with serverless functions
Configured areas live in YAML and currently include:
- Overview
- Account Portfolio
- Gnosis Pay
- Gnosis App
- Circles
- Trades
- Tokens
- OnChain Activity
- Consensus
- Network
- Bridges
- ESG
- Yields
- Revenue Drivers
- DAO Treasury (disabled)
This application follows a config-driven frontend + API architecture with server-side caching:
-
Frontend Dashboard - React application deployed to Vercel
- Resolves dashboard layout from
public/dashboard.ymlandpublic/dashboards/*.yml - Merges layout entries with metric definitions from
src/queries/*.js - Renders widgets/cards using ECharts and shared UI components
- Resolves dashboard layout from
-
API Proxy - Serverless API functions that interface with ClickHouse
- Handles authentication securely
- Implements caching to minimize ClickHouse queries
- Executes queries and returns results to the dashboard
- Deployed as Vercel Serverless Functions in the same project
-
Caching System - Reduces load on ClickHouse
- Uses
/tmpdirectory in Vercel functions for file-based caching - Falls back to in-memory caching if file operations fail
- Automatic daily refresh of cache data
- Configurable TTL (time to live) for cache entries
- Uses
-
Clone this repository:
git clone https://github.com/yourusername/clickhouse-metrics-dashboard.git cd clickhouse-metrics-dashboard -
Install dependencies (including API dependencies):
# Install project dependencies pnpm install -
Create
.envfile with your configuration:cp .env.example .env # Edit .env with your settings -
Start the development server:
pnpm dev
-
Run tests and production build:
pnpm test pnpm build
├── README.md
├── api/
│ ├── cache.js # Cache management implementation
│ ├── cron.js # Automatic refresh functionality
│ ├── metrics.js # Main API endpoint with caching support
│ ├── mock.js # Mock data generator
│ ├── test.js # Cache status API endpoint
│ ├── package.json # API dependencies
│ └── queries/ # Query definitions as JSON
├── public/ # Static assets
│ ├── dashboard.yml # Main sector index (order + icons + source)
│ └── dashboards/ # Per-sector layouts (metrics/tabs)
├── scripts/
│ └── export-queries.js # Script to export queries from frontend to API
├── src/
│ ├── components/
│ │ ├── Card.js # Card component
│ │ ├── Dashboard.js # Main dashboard component
│ │ ├── Header.js # Top header (search/resources/theme)
│ │ ├── MetricSearchBar.js # Header metric search dropdown
│ │ └── MetricWidget.js # Individual metric display
│ ├── services/
│ │ ├── api.js # API service with cache support
│ │ ├── dashboards.js # Dashboard/tab/layout resolution service
│ │ └── metrics.js # Metric config + data service
│ ├── queries/ # Frontend metric definitions
│ ├── utils/
│ │ ├── dashboardConfig.js # Loads and resolves YAML config
│ │ ├── metricSearch.js # Search index + scoring logic
│ │ ├── config.js # Application configuration
│ │ ├── dates.js # Date utilities
│ │ └── formatter.js # Value formatters
│ └── styles.css # Application styles
└── vercel.json # Vercel deployment configuration
The dashboard config is split into:
public/dashboard.ymlfor sector metadata and ordering.public/dashboards/<sector>.ymlfor each sector layout (metricsortabs).
Main file example:
Overview:
name: Overview
order: 1
icon: "📊"
iconClass: "chart-line"
source: /dashboards/overview.ymlSector file example:
metrics:
- id: overview_stake_api
gridRow: 1
gridColumn: 1 / span 3
minHeight: 130pxHow to add a new sector:
- Create
public/dashboards/<new-sector>.ymlwithmetricsortabs. - Add a top-level entry in
public/dashboard.ymlwithname,order,icon,iconClass, andsource. - Start the app and verify the new sector appears in navigation.
Tabs can define shared controls in YAML. These controls are owned by MetricGrid and then passed down to the affected widgets.
tabs:
- name: Per-token breakdown
globalFilterField: token
globalFilterLabel: Token
globalControlsPlacement: top
unitToggle: true
defaultUnit: native
timeRanges: true
defaultTimeRange: ALL
metrics:
- id: api_execution_tokens_supply_total
gridRow: 1
gridColumn: 1 / span 6
minHeight: 90pxSupported tab-level fields:
globalFilterField: field name shared across the tab, for exampletoken,token_class, orwallet_address.globalFilterLabel: optional UI label override. Defaults to a title-cased version ofglobalFilterField.globalControlsPlacement:gridortop.gridkeeps the historical in-gridglobal_filtercard.topmoves the tab-level filter bundle into the top toolbar next toDate range.unitToggle: enables the shared tab-levelNative / USDtoggle.defaultUnit: initial tab-level unit selection. Defaults tonative.timeRanges:truefor the default1M / 3M / 6M / 1Y / 2Y / ALLset, or an explicit array of labels.defaultTimeRange: initial selected tab-level range. Defaults toALL.searchable,searchPlaceholder: enable searchable top/global dropdowns for large option sets.globalFilterVertical: only affects the in-gridglobal_filtercard layout; ignored for top-toolbar placement.resolutionToggle,defaultResolution: enable shared resolution controls for metrics that expose multiple resolutions.
Notes:
global_filteris a pseudo-metric used only for layout. Use it only whenglobalControlsPlacement: grid.- When
globalControlsPlacement: top,MetricGridsuppresses any accidentalglobal_filterpseudo-metric and renders the global controls in the toolbar instead. - Global filter options are fetched once from a suitable metric in the tab and then reused across widgets.
Dashboards opt into named palette presets in public/dashboard.yml.
All palette definitions are centralized in:
src/utils/dashboardPalettes.js
GnosisPay:
name: Gnosis Pay
order: 10
icon: 💳
iconClass: credit-card
palette: gnosis-pay
source: /dashboards/gnosis-pay.yml- Scope is dashboard-level only (set once per top-level dashboard entry).
- Dashboard palette applies automatically to all chart and number widgets in that dashboard.
- Fallback-only precedence is enforced:
- Metric-level explicit color config wins (
color,colors,bandColors,lineColors, map-specific overrides, and explicit heatmap scale). - Dashboard palette is used only when metric-level colors are not explicitly defined.
- If dashboard palette is missing/invalid,
standardis used safely. - If a custom/dashboard palette has fewer colors than required series, the remaining series are filled from the
standardpalette before any repetition.
- Metric-level explicit color config wins (
palettein YAML accepts preset names only (for example:standard,gnosis-pay).
This is fully config-driven:
- Adding new dashboards/tabs/metrics in YAML requires no code changes to inherit palette behavior.
- Any metric placed in YAML under a dashboard automatically receives that dashboard palette fallback.
- New palette presets are added once in
src/utils/dashboardPalettes.jsand then referenced by name in YAML.
- Config load at startup
src/utils/dashboardConfig.jsloads/dashboard.yml.- If a sector defines
source, it loads and merges/dashboards/<sector>.yml. - Resolved YAML is passed to
src/services/dashboards.js.
- Layout resolution
- Each dashboard/tab metric ID is merged with its base metric config from
src/queries/*.js. - Grid placement (
gridRow,gridColumn,minHeight) comes from YAML.
- Each dashboard/tab metric ID is merged with its base metric config from
- Navigation state
src/components/Dashboard.jsmanages active dashboard/tab and syncs?dashboard=&tab=in the URL.
- Widget data flow
MetricWidgetloads metric config frommetricsService.- Data is fetched via
/api/metrics/:metricId. - Text widgets can render static content without API calls.
- Filters
- Global filters are tab-level controls configured in YAML with
globalFilterField. global_filteris only a pseudo-metric for in-grid placement; newer tabs can render the same controls at the top level withglobalControlsPlacement: top.- Global filter values are fetched once from a suitable metric and then reused across cards in the tab.
- Metrics participate in a global filter when they expose the same field as their
labelField, or when they explicitly setglobalFilterFieldin the metric config. - Local dropdowns still work for secondary filters when a widget has a different
labelFieldthan the tab-level global filter.
- Global filters are tab-level controls configured in YAML with
- Card text fields
description: subtitle shown under the card title.metricDescription: markdown content shown in the info popover.
- Chart controls
- Chart cards include info popover, PNG download, and expand-to-modal controls.
- Time ranges
- Tab-level time ranges are configured in YAML with
timeRanges. - Per-widget time ranges still come from metric
enableZoom, but they are hidden when a tab-level range is active. - Time range selection filters the dataset before it reaches number cards, tables, and charts; it does not rely on ECharts percentage zoom.
- Tab-level time ranges are configured in YAML with
- Token symbols and icons
- Dropdowns and global filters automatically render token icons when their field is
token. - Icon resolution is case-insensitive and comes from
src/utils/tokenIcons.js. - If a metric uses a different field name such as
label, it will not automatically receive token icons unless the UI explicitly passes an icon map.
- Dropdowns and global filters automatically render token icons when their field is
The header search is designed for fast tab jumps.
- Scope
- Search index is built only from resolved dashboard YAML metrics (
dashboard -> tab -> metrics). - Metrics that exist in
src/queriesbut are not placed in any YAML tab are intentionally excluded.
- Search index is built only from resolved dashboard YAML metrics (
- Exclusions
- Non-navigable pseudo entries like
global_filterare excluded.
- Non-navigable pseudo entries like
- Matching + ranking
- Highest priority: metric name exact/prefix/token matches.
- Then: metric ID token matches.
- Then: tab/dashboard/description/metricDescription context matches.
- Includes light typo tolerance (edit distance 1 for longer tokens).
- Result limits
- Default maximum is 8 results for speed and clarity.
- Duplicate labels
- If multiple results share the same
Metric Name + Dashboard/Tab, the UI appends a qualifier (description, or metricDescription, or metric ID fallback) to disambiguate.
- If multiple results share the same
- Navigation behavior
- Selecting a result jumps directly to its dashboard and tab (no card auto-scroll in this phase).
- Keyboard controls
ArrowUp/ArrowDownto move selectionEnterto navigateEscapeto close suggestions- Outside click closes suggestions
The top-bar Resources menu is fully config-driven from:
src/config/headerLinks.js
To add or update links, edit HEADER_RESOURCE_LINKS using the grouped structure below:
export const HEADER_RESOURCE_LINKS = [
{
id: 'api',
label: 'API',
links: [{ id: 'api-reference', label: 'API Reference', href: 'https://your-url' }]
}
];Each group renders a section in the dropdown, and each link opens in a new tab.
The dashboard implements a server-side caching system to minimize ClickHouse queries:
- First Request: On first request, the system queries ClickHouse and caches the results
- Subsequent Requests: Future requests use cached data (until cache expires)
- Automatic Refresh: Cache is automatically refreshed once per day
- Fallback Mechanism: If ClickHouse queries fail, system uses cached data
Configure caching behavior with these environment variables:
CACHE_TTL_HOURS: How long cache entries are valid (default: 24 hours)CACHE_REFRESH_HOURS: How often the cache is refreshed (default: 24 hours)
When deploying to Vercel's serverless environment:
- File System Limitations: The system uses
/tmpdirectory for caching - In-Memory Fallback: Falls back to in-memory caching if file operations fail
- Ephemeral Storage: Cache might reset between function invocations
Check cache status by visiting:
https://your-deployment-url/api/test
Include your API key in the X-API-Key header.
Force a cache refresh by adding refreshCache=true to your API requests:
https://your-deployment-url/api/metrics?refreshCache=true
- Local development (
NODE_ENV=development):- Frontend request caching is bypassed.
- Requests to
/api/metricsand/api/metrics/:idautomatically includeuseCached=falseunless explicitly provided. - API responses include
Cache-Control: no-store, max-age=0andPragma: no-cache.
- Production:
- Existing cache behavior is unchanged.
useCacheddefaults totruewhen omitted.
- Override support:
- You can explicitly set
useCached=trueoruseCached=falseper request in any environment.
- You can explicitly set
To deploy this dashboard, you'll need:
- A Vercel account
- A ClickHouse instance or ClickHouse Cloud account
- Your ClickHouse connection details
-
Install and log in to Vercel CLI:
npm install -g vercel vercel login
-
Deploy with Vercel:
vercel --prod
-
Configure environment variables in Vercel:
- Go to your project in the Vercel dashboard
- Navigate to Settings > Environment Variables
- Add the following environment variables:
CLICKHOUSE_HOST: Your ClickHouse host URLCLICKHOUSE_USER: ClickHouse usernameCLICKHOUSE_PASSWORD: ClickHouse passwordCLICKHOUSE_DATABASE(optional): Database nameCLICKHOUSE_DBT_SCHEMA(optional): dbt schema prefix used to rewritedbt.in queries (default:dbt)API_KEY: A secure key for API authenticationVITE_API_URL:/api(relative path)VITE_API_KEY: Same value asAPI_KEYVITE_DASHBOARD_TITLE(optional): Custom dashboard titleVITE_DEV_API_PROXY_TARGET(optional): Local API proxy target forpnpm devCACHE_TTL_HOURS(optional): Cache validity period in hoursCACHE_REFRESH_HOURS(optional): Cache refresh interval in hours
Vite-prefixed variables are the primary format:
VITE_API_URLVITE_API_KEYVITE_USE_MOCK_DATAVITE_DASHBOARD_TITLEVITE_PUBLIC_BASE_URL(optional)VITE_DEV_API_PROXY_TARGET(optional, local dev proxy)
Legacy REACT_APP_* variables are still supported as a temporary migration fallback.
If you encounter issues:
-
Check Vercel Logs:
vercel logs your-deployment-url
-
Check Cache Status: Visit
/api/testto see cache information. -
Use Mock Data Temporarily: Set
VITE_USE_MOCK_DATA=truein environment variables during testing.
To add a new metric:
-
Add a new metric query file in
src/queries/:// src/queries/newMetric.js const newMetric = { id: 'newMetricId', name: 'New Metric Name', description: 'Description of the new metric', format: 'formatNumber', // Use existing formatter or add new in formatter.js chartType: 'line', color: '#00BCD4', query: ` SELECT toDate(event_time) AS date, count() AS value FROM your_table WHERE event_time BETWEEN '{from}' AND '{to} 23:59:59' GROUP BY date ORDER BY date ` }; export default newMetric;
-
Ensure the metric is placed in dashboard YAML so it becomes visible and searchable:
- Add it to
public/dashboards/<sector>.ymlunder ametricslist in the target tab. - Metrics not placed in YAML are not rendered and are not included in header search.
- If the metric should participate in a tab-level global filter, make sure its filtering field matches the tab
globalFilterFieldor setglobalFilterFieldexplicitly in the metric config. - If the metric should inherit a tab-level unit toggle, define
unitFilterFieldorunitFields. - If the metric should show token icons in dropdowns, use
labelField: 'token'.
- Add it to
-
Run the export script to update the API:
pnpm run export-queries
-
Deploy your changes to Vercel:
vercel --prod
For local development without ClickHouse:
-
Set the
USE_MOCK_DATAenvironment variable totrue:USE_MOCK_DATA=true -
The API will generate mock data instead of querying ClickHouse.
This project is licensed under the MIT License.