-
-
Notifications
You must be signed in to change notification settings - Fork 833
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
base: dev
Are you sure you want to change the base?
Conversation
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.
- 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this change intentional?
There was a problem hiding this comment.
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
- 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.
feat(custom-api): add advanced array processing and function support
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) |
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 |
This commit enhances the custom-api widgets by adding support for both GET and POST HTTP methods. Key changes:
This implementation follows Glance documentation guidelines by avoiding the explicit method parameter and using only supported properties in the widget configuration.