Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(widgets): Add POST request support for custom-api widgets #454

Open
wants to merge 5 commits into
base: dev
Choose a base branch
from

Conversation

lawbyte
Copy link

@lawbyte lawbyte commented Mar 17, 2025

This commit enhances the custom-api widgets by adding support for both GET and POST HTTP methods. Key changes:

  • Auto-detect POST requests based on presence of body or form_data in widget config
  • Support JSON bodies with proper Content-Type header
  • Support form data submissions with application/x-www-form-urlencoded
  • Maintain backward compatibility with existing GET requests
  • Simplified request creation logic with better error handling
  • Updated client timeout to handle longer API responses

This implementation follows Glance documentation guidelines by avoiding the explicit method parameter and using only supported properties in the widget configuration.

lawbyte added 3 commits March 17, 2025 19:02
This commit enhances the custom-api widgets by adding support for both GET and POST HTTP methods. Key changes:

- Auto-detect POST requests based on presence of body or form_data in widget config
- Support JSON bodies with proper Content-Type header
- Support form data submissions with application/x-www-form-urlencoded
- Maintain backward compatibility with existing GET requests
- Simplified request creation logic with better error handling
- Updated client timeout to handle longer API responses

This implementation follows Glance documentation guidelines by avoiding the explicit method parameter and using only supported properties in the widget configuration.
Update custom-api widget documentation to include:
- Support for both GET and POST HTTP methods
- New body parameter for JSON POST requests
- New form_data parameter for form submissions
- Example configurations for both JSON and form data
- Documentation for cache parameter
- Clarification on parameter precedence

This documentation update complements the implementation changes in widget-custom-api.go for consistency.
@lawbyte
Copy link
Author

lawbyte commented Mar 17, 2025

#451

@lawbyte
Copy link
Author

lawbyte commented Mar 17, 2025

custom-api-preview-GET-POST

- name: API Examples
  columns:
    - size: full
      widgets:
        - type: search
          title: Search
          placeholder: Search...
          targets:
            - name: Google
              url: https://www.google.com/search?q={QUERY}
      
        # GET Request Example
        - type: custom-api
          title: GET Request Example
          url: http://localhost:5000
          headers:
            User-Agent: Glance Widget
            Accept: application/json
          template: |
            <div class="widget-content">
              <h3>GET Request Response</h3>
              <div class="response-details">
                <p><strong>Name:</strong> {{ .JSON.String "name" }}</p>
                <p><strong>Email:</strong> {{ .JSON.String "email" }}</p>
                <p><strong>Feedback:</strong> {{ .JSON.String "feedback" }}</p>
              </div>
            </div>
          cache: 1h
      
        # POST Request with JSON Body Example
        - type: custom-api
          title: POST with JSON Example
          url: http://localhost:5000
          headers:
            User-Agent: Glance Widget
            Accept: application/json
          body:
            message: "Hello from Glance!"
            timestamp: "2025-03-17T18:55:00+07:00"
            values:
              temperature: 25.5
              humidity: 60
          template: |
            <div class="widget-content">
              <h3>POST JSON Response</h3>
              <div class="response-details">
                <p><strong>Status:</strong> {{ .JSON.String "status" }}</p>
                <p><strong>Message:</strong> {{ .JSON.String "message" }}</p>
                <p><strong>Values:</strong> {{ .JSON.String "data.values.temperature" }}°C, {{ .JSON.String "data.values.humidity" }}%</p>
                <p><strong>Timestamp:</strong> {{ .JSON.String "data.timestamp" }}</p>
              </div>
            </div>
          cache: 1h
      
        # POST Request with Form Data Example
        - type: custom-api
          title: POST with Form Data Example
          url: http://localhost:5000
          headers:
            User-Agent: Glance Widget
            Accept: application/json
          form_data:
            name: "Glance Users"
            email: "[email protected]"
            feedback: "This widget is awesome!"
          template: |
            <div class="widget-content">
              <h3>POST Form Response</h3>
              <div class="response-details">
                <p><strong>Name:</strong> {{ .JSON.String "data.name" }}</p>
                <p><strong>Email:</strong> {{ .JSON.String "data.email" }}</p>
                <p><strong>Feedback:</strong> {{ .JSON.String "data.feedback" }}</p>
              </div>
            </div>
          cache: 1h
     

@@ -30,8 +30,6 @@
- [Server Stats](#server-stats)
- [Repository](#repository)
- [Bookmarks](#bookmarks)
- [Calendar](#calendar)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this change intentional?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah im sorry, its not intentional
you can ignore that

lawbyte added 2 commits March 18, 2025 17:09
- Implement function-based API calls with caching
- Add array processing functions: arrayLen, arrayItem, filter, intRange
- Support nested function calls in templates
- Add type-specific helpers: funcString, funcInt, funcFloat, funcBool
- Implement array existence checks with funcExists
- Add array iteration support with funcArray

This update enhances the custom-api widget's capabilities for handling complex data structures and multiple API endpoints.
@lawbyte
Copy link
Author

lawbyte commented Mar 18, 2025

feat(custom-api): add advanced array processing and function support

  • Implement function-based API calls with caching
  • Add array processing functions: arrayLen, arrayItem, filter, intRange
  • Support nested function calls in templates
  • Add type-specific helpers: funcString, funcInt, funcFloat, funcBool
  • Implement array existence checks with funcExists
  • Add array iteration support with funcArray

This update enhances the custom-api widget's capabilities for handling complex data structures and multiple API endpoints.

below is the yaml template for this:

- name: API Examples
  columns:
    - size: full
      widgets:
        - type: search
          title: Search
          placeholder: Search...
          targets:
            - name: Google
              url: https://www.google.com/search?q={QUERY}
      
        # GET Request Example
        - type: custom-api
          title: GET Request Example
          url: http://localhost:5000
          headers:
            User-Agent: Glance Widget
            Accept: application/json
          template: |
            <div class="widget-content">
              <h3>GET Request Response</h3>
              <div class="response-details">
                <p><strong>Name:</strong> {{ .JSON.String "name" }}</p>
                <p><strong>Email:</strong> {{ .JSON.String "email" }}</p>
                <p><strong>Feedback:</strong> {{ .JSON.String "feedback" }}</p>
              </div>
            </div>
          cache: 1h
      
        # POST Request with JSON Body Example
        - type: custom-api
          title: POST with JSON Example
          url: http://localhost:5000
          headers:
            User-Agent: Glance Widget
            Accept: application/json
          body:
            message: "Hello from Glance!"
            timestamp: "2025-03-17T18:55:00+07:00"
            values:
              temperature: 25.5
              humidity: 60
          template: |
            <div class="widget-content">
              <h3>POST JSON Response</h3>
              <div class="response-details">
                <p><strong>Status:</strong> {{ .JSON.String "status" }}</p>
                <p><strong>Message:</strong> {{ .JSON.String "message" }}</p>
                <p><strong>Values:</strong> {{ .JSON.String "data.values.temperature" }}°C, {{ .JSON.String "data.values.humidity" }}%</p>
                <p><strong>Timestamp:</strong> {{ .JSON.String "data.timestamp" }}</p>
              </div>
            </div>
          cache: 1h
      
        # POST Request with Form Data Example
        - type: custom-api
          title: POST with Form Data Example
          url: http://localhost:5000
          headers:
            User-Agent: Glance Widget
            Accept: application/json
          form_data:
            name: "Glance Users"
            email: "[email protected]"
            feedback: "This widget is awesome!"
          template: |
            <div class="widget-content">
              <h3>POST Form Response</h3>
              <div class="response-details">
                <p><strong>Name:</strong> {{ .JSON.String "data.name" }}</p>
                <p><strong>Email:</strong> {{ .JSON.String "data.email" }}</p>
                <p><strong>Feedback:</strong> {{ .JSON.String "data.feedback" }}</p>
              </div>
            </div>
          cache: 1h

        # Example of using functions in custom-api widgets
        - type: custom-api
          title: Combined API Data
          url: http://localhost:5000/primary-endpoint
          headers:
            Authorization: Bearer your-token
          # Define reusable functions
          functions:
            weather:  # Function name
              url: http://localhost:5000/weather/current
              headers:
                User-Agent: Glance Widget
              cache: 1h  # Cache for 1 hour
            user_data:
              url: http://localhost:5000/user/profile
              headers:
                Authorization: Bearer your-token
              cache: 30m  # Cache for 30 minutes
          template: |
            <div class="widget-content">
              <h3>Dashboard</h3>
              <!-- Access primary response data as usual -->
              <p>Status: {{ .JSON.String "status" }}</p>
              
              <!-- Access function response data -->
              <div class="weather-card">
                <h4>Current Weather</h4>
                <p>Temperature: {{ funcFloat . "weather" "main.temp" }}°C</p>
                <p>Condition: {{ funcString . "weather" "weather.0.description" }}</p>
              </div>
              
              <div class="user-info">
                <h4>User Profile</h4>
                <p>Username: {{ funcString . "user_data" "username" }}</p>
                <p>Email: {{ funcString . "user_data" "email" }}</p>
                <!-- Check if value exists before displaying -->
                {{ if funcExists . "user_data" "subscription" }}
                  <p>Subscription: {{ funcString . "user_data" "subscription.plan" }}</p>
                {{ end }}
              </div>
            </div>
          cache: 15m

        # Example of a Zoom meeting scheduler widget similar to zoom_scheduler.py
        - type: custom-api
          title: Zoom Meeting Scheduler
          url: http://localhost:5000/primary-endpoint
          functions:
            meetings:
              url: http://localhost:5000/meetings
              headers:
                Authorization: Bearer your-token
              cache: 1h  # Cache for 1 hour
            user_profile:
              url: http://localhost:5000/user/profile
              headers:
                Authorization: Bearer your-token
              cache: 30m  # Cache for 30 minutes
          template: |
            <style>
              body {
                background-color: #121212; /* Warna latar belakang utama */
                color: #e0e0e0; /* Warna teks utama */
                font-family: Arial, sans-serif;
              }

              .zoom-scheduler {
                padding: 20px;
                background-color: #1e1e1e; /* Warna latar belakang kartu */
                border-radius: 10px;anj w
                box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.5);
                max-width: 900px;
                margin: auto;
              }

              h3 {
                color: #90caf9; /* Warna biru terang untuk judul */
                text-align: center;
                margin-bottom: 20px;
              }

              .meeting-grid {
                display: grid;
                grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
                gap: 20px;
                margin-top: 20px;
              }

              .meeting-item {
                border: 1px solid #333; /* Garis pembatas yang lebih gelap */
                border-radius: 8px;
                padding: 15px;
                background-color: #212121; /* Warna latar belakang kotak */
                box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.7);
                transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
              }

              .meeting-item:hover {
                transform: translateY(-5px);
                box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.9);
              }

              .meeting-title {
                font-weight: bold;
                font-size: 1.2em;
                color: #ffffff; /* Warna teks putih */
                margin-bottom: 10px;
              }

              .meeting-time {
                font-size: 0.9em;
                color: #bdbdbd; /* Warna teks abu-abu terang */
                margin-bottom: 15px;
              }

              .join-button {
                display: inline-block;
                padding: 10px;
                background-color: #1976d2; /* Biru terang untuk tombol */
                color: white;
                text-decoration: none;
                border-radius: 5px;
                text-align: center;
                font-size: 0.9em;
                font-weight: bold;
                transition: background-color 0.3s ease-in-out, transform 0.2s ease-in-out;
                width: fit-content; /* Menyesuaikan ukuran tombol dengan konten */
                margin-top: auto; /* Menekan tombol ke bawah */
              }

              .join-button:hover {
                background-color: #1565c0; /* Biru lebih gelap untuk hover */
                transform: scale(1.05);
              }

              .pending {
                color: #f44336; /* Merah terang untuk status pending */
                font-weight: bold;
                text-align: center; /* Teks rata tengah */
              }
            </style>
            
            {{ $root := . }}
            <div class="zoom-scheduler">
              <h3>Today's Zoom Meetings</h3>
              
              <!-- Variables to track meetings -->
              {{ $todayCount := 0.0 }}
              {{ $videoConferencesToday := false }}
              
              <!-- Process today's video conference meetings -->
              {{ $meetings := funcArray $root "meetings" "meetings" }}
              {{ if $meetings }}
                {{ range $i, $meeting_group := $meetings }}
                  {{ $schedules := funcArray $root "meetings" (printf "meetings.%d.Schedule" $i) }}
                  {{ if $schedules }}
                    {{ range $j, $meeting := $schedules }}
                      {{ $delivery := funcString $root "meetings" (printf "meetings.%d.Schedule.%d.deliveryModeDesc" $i $j) }}
                      {{ $date := funcString $root "meetings" (printf "meetings.%d.Schedule.%d.date" $i $j) }}
                      {{ if eq $delivery "Video Conference" }}
                        {{ if eq $date "2025-03-18" }}
                          {{ $videoConferencesToday = true }}
                          {{ $todayCount = add $todayCount 1.0 }}
                          <div class="meeting-item">
                            <div class="meeting-title">{{ funcString $root "meetings" (printf "meetings.%d.Schedule.%d.content" $i $j) }}</div>
                            <div class="meeting-time">{{ funcString $root "meetings" (printf "meetings.%d.Schedule.%d.dateStart" $i $j) }}</div>
                            {{ $hasJoinUrl := funcExists $root "meetings" (printf "meetings.%d.Schedule.%d.joinUrl" $i $j) }}
                            {{ if $hasJoinUrl }}
                              <a href="{{ funcString $root "meetings" (printf "meetings.%d.Schedule.%d.joinUrl" $i $j) }}" class="join-button">Join Meeting</a>
                            {{ else }}
                              <span class="pending">URL not available yet</span>
                            {{ end }}
                          </div>
                        {{ end }}
                      {{ end }}
                    {{ end }}
                  {{ end }}
                {{ end }}
              {{ end }}
              
              <div class="meeting-count">
                {{ if not $videoConferencesToday }}
                  <p>No meetings scheduled for today</p>
                {{ else }}
                  <p>You have {{ $todayCount }} meeting(s) scheduled for today</p>
                {{ end }}
              </div>
              
              <h3>Upcoming Meetings This Week</h3>
              
              <!-- Process upcoming video conference meetings -->
              {{ $upcomingCount := 0.0 }}
              {{ $meetings := funcArray $root "meetings" "meetings" }}
              {{ if $meetings }}
                {{ range $i, $meeting_group := $meetings }}
                  {{ $schedules := funcArray $root "meetings" (printf "meetings.%d.Schedule" $i) }}
                  {{ if $schedules }}
                    {{ range $j, $meeting := $schedules }}
                      {{ $delivery := funcString $root "meetings" (printf "meetings.%d.Schedule.%d.deliveryModeDesc" $i $j) }}
                      {{ $date := funcString $root "meetings" (printf "meetings.%d.Schedule.%d.date" $i $j) }}
                      {{ if eq $delivery "Video Conference" }}
                        {{ if ne $date "2025-03-18" }}
                          {{ $upcomingCount = add $upcomingCount 1.0 }}
                          <div class="upcoming-item">
                            <div class="meeting-date">{{ funcString $root "meetings" (printf "meetings.%d.Schedule.%d.date" $i $j) }}</div>
                            <div class="meeting-title">{{ funcString $root "meetings" (printf "meetings.%d.Schedule.%d.content" $i $j) }}</div>
                          </div>
                        {{ end }}
                      {{ end }}
                    {{ end }}
                  {{ end }}
                {{ end }}
              {{ end }}
              
              {{ if eq $upcomingCount 0.0 }}
                <p>No upcoming meetings this week</p>
              {{ end }}
            </div>
          cache: 15m

and below is the server.py for mockup the post req:

from flask import Flask, request, jsonify
import json
from datetime import datetime, timedelta

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def handle_request():
    if request.method == 'GET':
        response = {
            "email": "[email protected]",
            "feedback": "This widget is awesome!",
            "name": "Glance User"
        }
    elif request.method == 'POST':
        try:
            if request.is_json:
                data = request.get_json()
            elif request.form:
                data = request.form.to_dict()
            else:
                data = json.loads(request.data.decode('utf-8'))
            
            response = {
                "status": "success",
                "message": "POST request received",
                "data": data
            }
        except Exception as e:
            response = {
                "status": "error",
                "message": f"Failed to parse data: {str(e)}",
                "data": None
            }
    return jsonify(response)

@app.route('/weather/current', methods=['GET'])
def weather():
    """Endpoint for weather data that matches the expected format in the widget"""
    weather_data = {
        "main": {
            "temp": 23.5,
            "feels_like": 24.2,
            "humidity": 68,
            "pressure": 1012
        },
        "weather": [
            {
                "id": 800,
                "main": "Clear",
                "description": "clear sky",
                "icon": "01d"
            }
        ],
        "wind": {
            "speed": 3.6,
            "deg": 160
        },
        "name": "Example City",
        "dt": int(datetime.now().timestamp()),
        "sys": {
            "country": "EX",
            "sunrise": int(datetime.now().timestamp()) - 21600,  # 6 hours ago
            "sunset": int(datetime.now().timestamp()) + 21600    # 6 hours from now
        }
    }
    return jsonify(weather_data)

@app.route('/user/profile', methods=['GET'])
def user_profile():
    """Endpoint for user profile data that matches the expected format in the widget"""
    user_data = {
        "username": "glance_user",
        "email": "[email protected]",
        "first_name": "Glance",
        "last_name": "User",
        "subscription": {
            "plan": "Premium",
            "expires_at": "2025-12-31T23:59:59Z",
            "features": ["dashboard", "widgets", "analytics"]
        },
        "preferences": {
            "theme": "dark",
            "notifications": True
        },
        "created_at": "2023-01-15T08:30:00Z",
        "last_login": "2025-03-18T02:30:00Z"
    }
    return jsonify(user_data)

@app.route('/primary-endpoint', methods=['GET'])
def primary_endpoint():
    """Main endpoint for the combined API data widget"""
    primary_data = {
        "status": "active",
        "message": "System operational",
        "timestamp": datetime.now().isoformat(),
        "version": "1.2.3",
        "services": {
            "api": "healthy",
            "database": "healthy",
            "cache": "healthy"
        }
    }
    return jsonify(primary_data)

@app.route('/meetings', methods=['GET'])
def meetings():
    """Endpoint that simulates the Zoom meeting data structure from the Python script"""
    now = datetime.now()
    today = now.date()
    today_str = today.isoformat()  # e.g., "2025-03-18"
    
    # Create test meetings for today and upcoming days
    schedule_data = [
        {
            "Schedule": [
                {
                    "dateStart": (now + timedelta(minutes=30)).isoformat(),
                    "dateEnd": (now + timedelta(hours=1, minutes=30)).isoformat(),
                    "date": today_str,  # Today's date
                    "deliveryModeDesc": "Video Conference",
                    "content": "Data Structures & Algorithms",
                    "joinUrl": "https://org.zoom.us/j/917667101234"
                },
                {
                    "dateStart": (now + timedelta(hours=3)).isoformat(),
                    "dateEnd": (now + timedelta(hours=5)).isoformat(),
                    "date": today_str,  # Today's date
                    "deliveryModeDesc": "Video Conference",
                    "content": "Artificial Intelligence",
                    "joinUrl": "https://org.zoom.us/j/82877821fb60"
                },
                {
                    "dateStart": (now + timedelta(days=1)).isoformat(),
                    "dateEnd": (now + timedelta(days=1, hours=2)).isoformat(),
                    "date": (today + timedelta(days=1)).isoformat(),  # Tomorrow
                    "deliveryModeDesc": "Video Conference",
                    "content": "Web Programming",
                    # No joinUrl yet for this meeting
                },
                {
                    "dateStart": (now + timedelta(days=2)).isoformat(),
                    "dateEnd": (now + timedelta(days=2, hours=3)).isoformat(),
                    "date": (today + timedelta(days=2)).isoformat(),  # Day after tomorrow
                    "deliveryModeDesc": "In-Class",  # Not a video conference
                    "content": "Computer Graphics"
                },
                {
                    "dateStart": (now + timedelta(days=3)).isoformat(),
                    "dateEnd": (now + timedelta(days=3, hours=2)).isoformat(),
                    "date": (today + timedelta(days=3)).isoformat(),  # 3 days later
                    "deliveryModeDesc": "Video Conference",
                    "content": "Database Systems",
                    "joinUrl": "https://org.zoom.us/j/15100154fe83"
                }
            ]
        }
    ]
    
    # Return in the format expected by the Glance widget template
    response_data = {
        "meetings": schedule_data
    }
    
    return jsonify(response_data)

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=5000)

@svilenmarkov
Copy link
Member

Hey, thanks for contributing!

This PR adds a bit too many things all at once. Most of the template functions aren't necessary since those things are already possible thanks to tidwall/gjson. Could you please slim it down to just the POST functionality? Also, there are conflicts since new functionality was added on the dev branch, which is where this should be getting merged.

@svilenmarkov svilenmarkov changed the base branch from main to dev March 18, 2025 23:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants