Using AWS Cloudscape Design System to create a simple cost analysis dashboard

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:

  1. Fetches and displays AWS service costs over time
  2. 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
  3. Provides interactive filters:
    • Date range selector for time period
    • Granularity toggle (Daily/Monthly)
    • Refresh button to update data
  4. 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.

aws style cost-analysis dashboard

Key Cloudscape Components Used:

  1. SpaceBetween
    • Purpose: Manages spacing between components
    • Usage: Wraps content with consistent spacing
  2. Container
    • Purpose: Provides a styled container with optional header
    • Usage: Wraps sections of content
  3. Header
    • Purpose: Provides consistent heading styles
    • Usage: Section titles and container headers
  4. Button
    • Purpose: Standard button component
    • Usage: Action triggers (e.g., Refresh)
  5. Grid
    • Purpose: Layout management
    • Usage: Arranges components in a grid system
  6. DateRangePicker
    • Purpose: Date range selection
    • Usage: Selecting time periods for cost analysis
  7. Select
    • Purpose: Dropdown selection
    • Usage: Granularity selection (Daily/Monthly)
  8. PieChart
    • Purpose: Circular data visualization
    • Usage: Service cost distribution
  9. BarChart
    • Purpose: Bar graph visualization
    • Usage: Cost trends over time
  10. 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:

  1. Top-level navigation
  2. Filters section
  3. Visual data (charts)
  4. Detailed data (table)

To use this component:

  1. Install required dependencies:
npm install @cloudscape-design/components @cloudscape-design/collection-hooks
  1. 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'
    )