Category: articles

  • 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'
        )