Skip to main content

API Mashup

Combine datasets, integrations, and saved queries in a single app with responsive layouts and loading states.

What You'll Build

A comprehensive dashboard that:

  • Queries a dataset (orders)
  • Executes a saved query (monthly summary)
  • Calls an integration (partner API)
  • Shows loading states for each data source
  • Handles errors gracefully
  • Uses a multi-panel responsive layout

Prerequisites

Example Prompts for Claude Code

Build the mashup:

"Create an executive dashboard with 3 panels. First panel queries admin:orders dataset for revenue metrics. Second panel executes the admin:monthly-summary saved query and shows it as a line chart. Third panel calls the partner-api integration. Each panel should show its own loading status. Load all three in parallel using Promise.allSettled. Update informer.yaml to grant access to all three sources."

Add polish:

"Add a status indicator (loading/success/error) to each panel header. Use color-coded badges."

"Make the layout responsive — stack panels vertically on mobile, show 2 columns on desktop."

Improve error handling:

"If one data source fails, the others should still work. Show a friendly error message in the failed panel but keep the rest of the dashboard functional."

What Claude Will Generate

Claude will create a multi-panel layout with parallel data loading. Here's what the generated code looks like:

Data Access Configuration

datasets:
- admin:orders

queries:
- admin:monthly-summary

integrations:
- partner-api

Multi-Panel HTML

Claude will scaffold a panel-based layout:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Executive Dashboard</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="dashboard">
<header>
<h1>Executive Dashboard</h1>
<p class="subtitle">Real-time data from multiple sources</p>
</header>

<div class="filters">
<div class="filter-group">
<label for="dateRange">Date Range</label>
<select id="dateRange">
<option value="30" selected>Last 30 days</option>
<option value="90">Last 90 days</option>
<option value="365">Last year</option>
</select>
</div>
<button id="refresh" class="btn-primary">Refresh</button>
</div>

<!-- Orders Panel -->
<div class="panel">
<div class="panel-header">
<h2>Orders</h2>
<span class="status" id="ordersStatus">Loading...</span>
</div>
<div class="metrics">
<div class="metric-card">
<span class="metric-value" id="ordersRevenue">--</span>
<span class="metric-label">Revenue</span>
</div>
<div class="metric-card">
<span class="metric-value" id="ordersCount">--</span>
<span class="metric-label">Count</span>
</div>
</div>
</div>

<!-- Monthly Summary Panel (Saved Query) -->
<div class="panel">
<div class="panel-header">
<h2>Monthly Summary</h2>
<span class="status" id="summaryStatus">Loading...</span>
</div>
<div id="summaryChart" style="width: 100%; height: 300px;"></div>
</div>

<!-- Partner Sales Panel (Integration) -->
<div class="panel">
<div class="panel-header">
<h2>Partner Sales</h2>
<span class="status" id="partnerStatus">Loading...</span>
</div>
<div class="metrics">
<div class="metric-card">
<span class="metric-value" id="partnerRevenue">--</span>
<span class="metric-label">Revenue</span>
</div>
<div class="metric-card">
<span class="metric-value" id="partnerCount">--</span>
<span class="metric-label">Orders</span>
</div>
</div>
</div>

<!-- Combined Chart -->
<div class="panel panel-wide">
<div class="panel-header">
<h2>Revenue Breakdown</h2>
</div>
<div id="combinedChart" style="width: 100%; height: 400px;"></div>
</div>
</div>

<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<script type="module" src="main.js"></script>
</body>
</html>

Panel Styles

Claude will add responsive panel styling with status indicators:

/* ... previous styles ... */

.panel {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
}

.panel-wide {
grid-column: 1 / -1;
}

.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}

.panel-header h2 {
font-size: 18px;
font-weight: 600;
}

.status {
font-size: 13px;
padding: 4px 10px;
border-radius: 12px;
font-weight: 500;
}

.status.loading {
background: #fef3c7;
color: #92400e;
}

.status.success {
background: #d1fae5;
color: #065f46;
}

.status.error {
background: #fee2e2;
color: #991b1b;
}

[data-theme="dark"] .status.loading {
background: #78350f;
color: #fef3c7;
}

[data-theme="dark"] .status.success {
background: #064e3b;
color: #d1fae5;
}

[data-theme="dark"] .status.error {
background: #7f1d1d;
color: #fee2e2;
}

@media (min-width: 1024px) {
.dashboard {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
}

header,
.filters,
.panel-wide {
grid-column: 1 / -1;
}
}

@media print {
.filters { display: none; }
.status { display: none; }
}

Parallel Data Loading

Claude will implement Promise.allSettled for resilient parallel loading:

window.informerReady = false;

const theme = window.__INFORMER__?.theme || 'light';
document.documentElement.setAttribute('data-theme', theme);

const ORDERS_DATASET = 'admin:orders';
const MONTHLY_QUERY = 'admin:monthly-summary';
const PARTNER_INTEGRATION = 'partner-api';

let currentFilters = { dateRange: '30' };

async function init() {
try {
// Wire up controls
document.getElementById('refresh').addEventListener('click', loadAllData);
document.getElementById('dateRange').addEventListener('change', (e) => {
currentFilters.dateRange = e.target.value;
});

// Load all data sources
await loadAllData();

window.informerReady = true;
} catch (err) {
console.error('Initialization failed:', err);
}
}

async function loadAllData() {
// Load all data sources in parallel
await Promise.allSettled([
loadOrders(),
loadMonthlySummary(),
loadPartnerSales()
]);

// After all panels load, render combined chart
renderCombinedChart();
}

async function loadOrders() {
setStatus('ordersStatus', 'loading');

try {
const dateFilter = buildDateFilter();
const filters = [];

if (dateFilter) {
filters.push({
range: {
orderDate: {
gte: dateFilter.start,
lte: dateFilter.end
}
}
});
}

const response = await fetch(`/api/datasets/${ORDERS_DATASET}/_search`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: { bool: { filter: filters } },
size: 0,
aggs: {
totalRevenue: { sum: { field: 'amount' } },
orderCount: { value_count: { field: 'id' } }
}
})
});

if (!response.ok) throw new Error(`Orders query failed: ${response.status}`);

const result = await response.json();
const aggs = result.aggregations;

document.getElementById('ordersRevenue').textContent = formatCurrency(aggs.totalRevenue.value);
document.getElementById('ordersCount').textContent = aggs.orderCount.value.toLocaleString();

setStatus('ordersStatus', 'success', 'Loaded');

// Store for combined chart
window.ordersData = {
revenue: aggs.totalRevenue.value,
count: aggs.orderCount.value
};
} catch (err) {
console.error('Orders error:', err);
setStatus('ordersStatus', 'error', 'Failed');
document.getElementById('ordersRevenue').textContent = '--';
document.getElementById('ordersCount').textContent = '--';
window.ordersData = { revenue: 0, count: 0 };
}
}

async function loadMonthlySummary() {
setStatus('summaryStatus', 'loading');

try {
const response = await fetch(`/api/queries/${MONTHLY_QUERY}/_execute`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
parameters: {
days: parseInt(currentFilters.dateRange)
}
})
});

if (!response.ok) throw new Error(`Query execution failed: ${response.status}`);

const result = await response.json();

// Parse query results
// Assume result format: { data: [{ month: 'Jan', revenue: 12345 }, ...] }
const data = result.data || [];

renderSummaryChart(data);
setStatus('summaryStatus', 'success', 'Loaded');

// Store for combined chart
window.summaryData = data;
} catch (err) {
console.error('Summary query error:', err);
setStatus('summaryStatus', 'error', 'Failed');
window.summaryData = [];
}
}

async function loadPartnerSales() {
setStatus('partnerStatus', 'loading');

try {
const dateFilter = buildDateFilter();
const params = {};
if (dateFilter) {
params.start_date = dateFilter.start;
params.end_date = dateFilter.end;
}

const response = await fetch(`/api/integrations/${PARTNER_INTEGRATION}/request`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: '/v1/sales',
method: 'GET',
params
})
});

if (!response.ok) throw new Error(`Integration request failed: ${response.status}`);

const result = await response.json();

if (result.error) {
throw new Error(`Partner API error: ${result.status}`);
}

// Parse partner response
const revenue = result.data.total_revenue || 0;
const count = result.data.count || 0;

document.getElementById('partnerRevenue').textContent = formatCurrency(revenue);
document.getElementById('partnerCount').textContent = count.toLocaleString();

setStatus('partnerStatus', 'success', 'Loaded');

// Store for combined chart
window.partnerData = { revenue, count };
} catch (err) {
console.error('Partner API error:', err);
setStatus('partnerStatus', 'error', 'Failed');
document.getElementById('partnerRevenue').textContent = '--';
document.getElementById('partnerCount').textContent = '--';
window.partnerData = { revenue: 0, count: 0 };
}
}

function renderSummaryChart(data) {
const chart = echarts.init(document.getElementById('summaryChart'), theme === 'dark' ? 'dark' : null);

const months = data.map(d => d.month);
const revenues = data.map(d => d.revenue);

chart.setOption({
tooltip: { trigger: 'axis' },
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: months
},
yAxis: {
type: 'value',
axisLabel: {
formatter: (val) => {
if (val >= 1000000) return `$${(val / 1000000).toFixed(1)}M`;
if (val >= 1000) return `$${(val / 1000).toFixed(0)}K`;
return `$${val}`;
}
}
},
series: [{
name: 'Revenue',
type: 'line',
data: revenues,
smooth: true,
itemStyle: { color: '#6366f1' }
}]
});

window.addEventListener('resize', () => chart.resize());
}

function renderCombinedChart() {
const chart = echarts.init(document.getElementById('combinedChart'), theme === 'dark' ? 'dark' : null);

const ordersRev = window.ordersData?.revenue || 0;
const partnerRev = window.partnerData?.revenue || 0;

chart.setOption({
tooltip: {
trigger: 'item',
formatter: (params) => {
return `${params.name}<br/>${formatCurrency(params.value)} (${params.percent}%)`;
}
},
legend: { top: '5%', left: 'center' },
series: [{
name: 'Revenue Source',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: theme === 'dark' ? '#1e293b' : '#fff',
borderWidth: 2
},
label: { show: false },
emphasis: {
label: {
show: true,
fontSize: 18,
fontWeight: 'bold'
}
},
data: [
{ value: ordersRev, name: 'Internal Orders' },
{ value: partnerRev, name: 'Partner Sales' }
]
}]
});

window.addEventListener('resize', () => chart.resize());
}

function buildDateFilter() {
if (currentFilters.dateRange === 'all') return null;

const days = parseInt(currentFilters.dateRange);
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - days);

return {
start: startDate.toISOString().split('T')[0],
end: endDate.toISOString().split('T')[0]
};
}

function setStatus(elementId, state, text) {
const el = document.getElementById(elementId);
el.className = `status ${state}`;
el.textContent = text || state;
}

function formatCurrency(val) {
if (isNaN(val)) return '--';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0
}).format(val);
}

init();

Query Execution Pattern

Saved queries are executed via POST /api/queries/\{id\}/_execute:

const response = await fetch(`/api/queries/${queryId}/_execute`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
parameters: {
startDate: '2024-01-01',
endDate: '2024-12-31',
region: 'North'
}
})
});

const result = await response.json();
// result.data = [{ col1: val1, col2: val2 }, ...]

Parameters are defined in the query's configuration and can be:

  • Text - Free-form strings
  • Number - Numeric values
  • Date - ISO date strings
  • List - Single or multi-select values

Loading States

The dashboard uses three status states:

setStatus('ordersStatus', 'loading');  // Yellow: data loading
setStatus('ordersStatus', 'success', 'Loaded'); // Green: success
setStatus('ordersStatus', 'error', 'Failed'); // Red: error

This provides visual feedback for each data source independently.

Error Resilience

Using Promise.allSettled ensures all panels load even if one fails:

await Promise.allSettled([
loadOrders(), // Might fail
loadMonthlySummary(), // Might fail
loadPartnerSales() // Might fail
]);

// Continue rendering combined chart with whatever data loaded
renderCombinedChart();

Without allSettled, a single panel failure would break the entire dashboard.

Test Locally

npm run dev

Verify:

  • All three panels load in parallel
  • Status indicators update correctly
  • Errors are handled gracefully
  • Combined chart renders after all data loads

Deploy

npm run deploy

Next Steps

Continue to AI-Enhanced Dashboard to add AI capabilities with tools and chat integration.