I am not an expert in frontend design or development. However, I am impressed with AWS UI and how it’s look and feel. This has lead me to find out about Cloudscape.
Cloudscape
An open source design system for the cloud
Cloudscape offers user interface guidelines, front-end components, design resources, and development tools for building intuitive, engaging, and inclusive user experiences at scale.
This article explains frontend typescript, which creates an AWS-style cost analysis dashboard that:
- Fetches and displays AWS service costs over time
- Shows cost data in three main views:
- Pie chart for service cost distribution
- Bar chart for cost trends over time
- Table for detailed cost breakdown
- Provides interactive filters:
- Date range selector for time period
- Granularity toggle (Daily/Monthly)
- Refresh button to update data
- Features:
- Real-time cost calculations
- Percentage breakdowns
- Currency formatting
- Loading states
- Responsive layout
It uses AWS Cloudscape Design System components to maintain AWS console look-and-feel, making it appear as a native AWS console page.

Key Cloudscape Components Used:
- SpaceBetween
- Purpose: Manages spacing between components
- Usage: Wraps content with consistent spacing
- Container
- Purpose: Provides a styled container with optional header
- Usage: Wraps sections of content
- Header
- Purpose: Provides consistent heading styles
- Usage: Section titles and container headers
- Button
- Purpose: Standard button component
- Usage: Action triggers (e.g., Refresh)
- Grid
- Purpose: Layout management
- Usage: Arranges components in a grid system
- DateRangePicker
- Purpose: Date range selection
- Usage: Selecting time periods for cost analysis
- Select
- Purpose: Dropdown selection
- Usage: Granularity selection (Daily/Monthly)
- PieChart
- Purpose: Circular data visualization
- Usage: Service cost distribution
- BarChart
- Purpose: Bar graph visualization
- Usage: Cost trends over time
- Table
- Purpose: Data table display
- Usage: Detailed cost breakdown
Benefits of Using Cloudscape:
- Consistent AWS-style UI
- Built-in accessibility
- Responsive design
- Consistent theming
- Built-in internationalization
- Performance optimized
- Enterprise-ready components
The layout follows AWS console patterns:
- Top-level navigation
- Filters section
- Visual data (charts)
- Detailed data (table)
To use this component:
- Install required dependencies:
npm install @cloudscape-design/components @cloudscape-design/collection-hooks
- Add Cloudscape styles to your application:
// In your app's entry point
import '@cloudscape-design/global-styles/index.css';
Frontend script
import React, { useState, useEffect, useCallback } from 'react';
import {
BarChart,
Button,
Container,
DateRangePicker,
Grid,
Header,
PieChart,
Select,
SpaceBetween,
Table,
} from '@cloudscape-design/components';
interface CostData {
date: string;
serviceName: string;
blendedCost: number;
}
interface ServiceTotal {
serviceName: string;
totalCost: number;
}
const AWSAllServicesCost: React.FC = () => {
const [costData, setCostData] = useState<CostData[]>([]);
const [serviceTotals, setServiceTotals] = useState<ServiceTotal[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [dateRange, setDateRange] = useState({
type: 'absolute' as const,
startDate: new Date(new Date().setMonth(new Date().getMonth() - 1)),
endDate: new Date()
});
const [granularity, setGranularity] = useState<'DAILY' | 'MONTHLY'>('DAILY');
const fetchAllServicesCost = useCallback(async () => {
try {
setLoading(true);
const response = await fetch('http://localhost:5001/api/costs', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
startDate: dateRange.startDate.toISOString().split('T')[0],
endDate: dateRange.endDate.toISOString().split('T')[0],
granularity: granularity,
}),
});
const data = await response.json();
setCostData(data.costsByDate);
// Calculate service totals
const totals = calculateServiceTotals(data.costsByDate);
setServiceTotals(totals);
} catch (error) {
console.error('Error fetching cost data:', error);
} finally {
setLoading(false);
}
}, [dateRange, granularity]);
const calculateServiceTotals = (data: CostData[]): ServiceTotal[] => {
const totals = data.reduce((acc: { [key: string]: number }, curr) => {
acc[curr.serviceName] = (acc[curr.serviceName] || 0) + curr.blendedCost;
return acc;
}, {});
return Object.entries(totals)
.map(([serviceName, totalCost]) => ({ serviceName, totalCost }))
.sort((a, b) => b.totalCost - a.totalCost)
.slice(0, 10); // Top 10 services
};
useEffect(() => {
fetchAllServicesCost();
}, [fetchAllServicesCost]);
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(value);
};
return (
<SpaceBetween size="l">
<Container
header={
<Header
variant="h1"
actions={
<Button onClick={fetchAllServicesCost} loading={loading}>
Refresh
</Button>
}
>
AWS Cost Analysis
</Header>
}
>
<SpaceBetween size="l">
{/* Filters */}
<Grid gridDefinition={[{ colspan: 8 }, { colspan: 4 }]}>
<DateRangePicker
value={dateRange}
onChange={({ detail }) => setDateRange(detail.value)}
relativeOptions={[
{
key: 'previous-30-days',
amount: 30,
unit: 'day',
type: 'relative'
},
{
key: 'previous-7-days',
amount: 7,
unit: 'day',
type: 'relative'
}
]}
i18nStrings={{
todayAriaLabel: 'Today',
nextMonthAriaLabel: 'Next month',
previousMonthAriaLabel: 'Previous month',
customRelativeRangeOptionLabel: 'Custom range',
customRelativeRangeOptionDescription: 'Set a custom range',
applyButtonLabel: 'Apply',
clearButtonLabel: 'Clear',
}}
/>
<Select
selectedOption={{ value: granularity, label: granularity }}
onChange={({ detail }) =>
setGranularity(detail.selectedOption.value as 'DAILY' | 'MONTHLY')
}
options={[
{ value: 'DAILY', label: 'Daily' },
{ value: 'MONTHLY', label: 'Monthly' }
]}
/>
</Grid>
{/* Charts */}
<Grid gridDefinition={[{ colspan: 6 }, { colspan: 6 }]}>
<Container header={<Header variant="h2">Service Cost Distribution</Header>}>
<PieChart
data={serviceTotals.map(service => ({
title: service.serviceName,
value: service.totalCost
}))}
i18nStrings={{
detailsValue: formatCurrency,
detailsPercentage: (value) => `${(value * 100).toFixed(0)}%`,
chartAriaRoleDescription: 'Pie chart',
}}
size="medium"
variant="donut"
hideFilter
hideLegend
innerMetricDescription="Total Cost"
innerMetricValue={formatCurrency(
serviceTotals.reduce((sum, service) => sum + service.totalCost, 0)
)}
/>
</Container>
<Container header={<Header variant="h2">Cost Trends</Header>}>
<BarChart
series={[
{
title: 'Cost',
type: 'bar',
data: costData.map(item => ({
x: new Date(item.date),
y: item.blendedCost
}))
}
]}
xDomain={[dateRange.startDate, dateRange.endDate]}
i18nStrings={{
xTickFormatter: (date) => date.toLocaleDateString(),
yTickFormatter: formatCurrency
}}
hideFilter
hideLegend
xScaleType="time"
/>
</Container>
</Grid>
{/* Cost Table */}
<Table
header={<Header variant="h2">Service Cost Breakdown</Header>}
columnDefinitions={[
{
id: 'service',
header: 'Service',
cell: item => item.serviceName,
sortingField: 'serviceName'
},
{
id: 'cost',
header: 'Total Cost',
cell: item => formatCurrency(item.totalCost),
sortingField: 'totalCost'
},
{
id: 'percentage',
header: 'Percentage',
cell: item => {
const total = serviceTotals.reduce((sum, service) => sum + service.totalCost, 0);
return `${((item.totalCost / total) * 100).toFixed(2)}%`;
}
}
]}
items={serviceTotals}
loading={loading}
loadingText="Loading cost data"
sortingDisabled
variant="container"
stickyHeader
/>
</SpaceBetween>
</Container>
</SpaceBetween>
);
};
export default AWSAllServicesCost;
Backend python script
# app.py
from flask import Flask, request, jsonify
from flask_cors import CORS
import boto3
from datetime import datetime, timedelta
from botocore.exceptions import ClientError
import os
from dotenv import load_dotenv
import logging
# Load environment variables
load_dotenv("./.env-local")
app = Flask(__name__)
CORS(app) # Enable CORS for all routes
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Initialize AWS Cost Explorer client
try:
ce_client = boto3.client('ce',
aws_access_key_id=os.getenv('AWS_ACCESS_KEY_ID'),
aws_secret_access_key=os.getenv('AWS_SECRET_ACCESS_KEY'),
region_name=os.getenv('AWS_REGION', 'eu-west-1')
)
except Exception as e:
logger.error(f"Failed to initialize AWS Cost Explorer client: {str(e)}")
raise
def validate_date_format(date_string: str) -> bool:
"""Validate if the date string matches YYYY-MM-DD format."""
try:
datetime.strptime(date_string, '%Y-%m-%d')
return True
except ValueError:
return False
@app.route('/api/costs', methods=['POST'])
def get_costs():
try:
data = request.get_json()
# Validate required fields
if not data or 'startDate' not in data or 'endDate' not in data:
return jsonify({
'error': 'Missing required parameters: startDate and endDate'
}), 400
start_date = data['startDate']
end_date = data['endDate']
granularity = data.get('granularity', 'DAILY') # Default to DAILY if not specified
# Validate date formats
if not validate_date_format(start_date) or not validate_date_format(end_date):
return jsonify({
'error': 'Invalid date format. Use YYYY-MM-DD'
}), 400
# Validate granularity
valid_granularities = ['DAILY', 'MONTHLY']
if granularity not in valid_granularities:
return jsonify({
'error': f'Invalid granularity. Must be one of: {", ".join(valid_granularities)}'
}), 400
# Get cost data from AWS Cost Explorer
response = ce_client.get_cost_and_usage(
TimePeriod={
'Start': start_date,
'End': end_date
},
Granularity=granularity,
Metrics=['BlendedCost'],
GroupBy=[
{'Type': 'DIMENSION', 'Key': 'SERVICE'}
]
)
# Transform the response data
cost_data = []
for time_period in response.get('ResultsByTime', []):
date = time_period['TimePeriod']['Start']
for group in time_period.get('Groups', []):
service_name = group['Keys'][0]
cost_amount = float(group['Metrics']['BlendedCost']['Amount'])
cost_data.append({
'date': date,
'serviceName': service_name,
'blendedCost': cost_amount
})
# Calculate service totals
service_totals = {}
for item in cost_data:
service_name = item['serviceName']
cost = item['blendedCost']
service_totals[service_name] = service_totals.get(service_name, 0) + cost
# Sort services by total cost and get top 10
top_services = sorted(
[{'serviceName': k, 'totalCost': v}
for k, v in service_totals.items()],
key=lambda x: x['totalCost'],
reverse=True
)[:10]
return jsonify({
'costsByDate': cost_data,
'topServices': top_services,
'message': 'Success'
})
except ClientError as e:
logger.error(f"AWS Cost Explorer API error: {str(e)}")
return jsonify({
'error': 'Failed to fetch cost data from AWS',
'message': str(e)
}), 500
except Exception as e:
logger.error(f"Unexpected error: {str(e)}")
return jsonify({
'error': 'Internal server error',
'message': str(e)
}), 500
@app.route('/api/cost-summary', methods=['GET'])
def get_cost_summary():
try:
# Get the last 30 days of cost data
end_date = datetime.now().strftime('%Y-%m-%d')
start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
response = ce_client.get_cost_and_usage(
TimePeriod={
'Start': start_date,
'End': end_date
},
Granularity='MONTHLY',
Metrics=['BlendedCost']
)
total_cost = sum(
float(period['Total']['BlendedCost']['Amount'])
for period in response['ResultsByTime']
)
return jsonify({
'totalCost': total_cost,
'period': {
'start': start_date,
'end': end_date
}
})
except Exception as e:
logger.error(f"Error fetching cost summary: {str(e)}")
return jsonify({
'error': 'Failed to fetch cost summary',
'message': str(e)
}), 500
@app.route('/api/service-trends', methods=['POST'])
def get_service_trends():
try:
data = request.get_json()
service_name = data.get('serviceName')
if not service_name:
return jsonify({
'error': 'Service name is required'
}), 400
end_date = datetime.now().strftime('%Y-%m-%d')
start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%-d')
response = ce_client.get_cost_and_usage(
TimePeriod={
'Start': start_date,
'End': end_date
},
Granularity='DAILY',
Metrics=['BlendedCost'],
Filter={
'Dimensions': {
'Key': 'SERVICE',
'Values': [service_name]
}
}
)
trends = [{
'date': period['TimePeriod']['Start'],
'cost': float(period['Total']['BlendedCost']['Amount'])
} for period in response['ResultsByTime']]
return jsonify({
'serviceName': service_name,
'trends': trends
})
except Exception as e:
logger.error(f"Error fetching service trends: {str(e)}")
return jsonify({
'error': 'Failed to fetch service trends',
'message': str(e)
}), 500
@app.route('/api/health', methods=['GET'])
def health_check():
"""Health check endpoint"""
return jsonify({
'status': 'healthy',
'timestamp': datetime.now().isoformat()
})
if __name__ == '__main__':
# Create .env file if it doesn't exist
if not os.path.exists('.env'):
logger.warning("No .env file found. Creating template...")
with open('.env', 'w') as f:
f.write("""AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=eu-west-1
FLASK_ENV=development
""")
logger.info("Created .env template. Please fill in your AWS credentials.")
# Get port from environment variable or default to 5000
port = int(os.getenv('PORT', 5001))
# Run the application
app.run(
host='0.0.0.0',
port=port,
debug=os.getenv('FLASK_ENV') == 'development'
)