update
This commit is contained in:
BIN
fusion_api/static/description/icon.png
Normal file
BIN
fusion_api/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
102
fusion_api/static/src/js/dashboard.js
Normal file
102
fusion_api/static/src/js/dashboard.js
Normal file
@@ -0,0 +1,102 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { Component, onWillStart, useState } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class FusionApiDashboard extends Component {
|
||||
static template = "fusion_api.Dashboard";
|
||||
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
this.actionService = useService("action");
|
||||
|
||||
this.state = useState({
|
||||
totalProviders: 0,
|
||||
activeProviders: 0,
|
||||
totalConsumers: 0,
|
||||
activeConsumers: 0,
|
||||
monthCost: 0,
|
||||
monthRequests: 0,
|
||||
todayRequests: 0,
|
||||
topConsumers: [],
|
||||
providerStats: [],
|
||||
recentUsage: [],
|
||||
approachingLimits: [],
|
||||
loaded: false,
|
||||
});
|
||||
|
||||
onWillStart(() => this.loadData());
|
||||
}
|
||||
|
||||
async loadData() {
|
||||
const data = await this.orm.call(
|
||||
"fusion.api.provider",
|
||||
"get_dashboard_data",
|
||||
[],
|
||||
);
|
||||
Object.assign(this.state, {
|
||||
totalProviders: data.total_providers,
|
||||
activeProviders: data.active_providers,
|
||||
totalConsumers: data.total_consumers,
|
||||
activeConsumers: data.active_consumers,
|
||||
monthCost: data.month_cost,
|
||||
monthRequests: data.month_requests,
|
||||
todayRequests: data.today_requests,
|
||||
topConsumers: data.top_consumers || [],
|
||||
providerStats: data.provider_stats || [],
|
||||
recentUsage: data.recent_usage || [],
|
||||
approachingLimits: data.approaching_limits || [],
|
||||
loaded: true,
|
||||
});
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
this.state.loaded = false;
|
||||
await this.loadData();
|
||||
}
|
||||
|
||||
openProviders() {
|
||||
this.actionService.doAction("fusion_api.action_api_provider");
|
||||
}
|
||||
|
||||
openConsumers() {
|
||||
this.actionService.doAction("fusion_api.action_api_consumer");
|
||||
}
|
||||
|
||||
openUsageLog() {
|
||||
this.actionService.doAction("fusion_api.action_api_usage");
|
||||
}
|
||||
|
||||
openAccessRules() {
|
||||
this.actionService.doAction("fusion_api.action_api_access");
|
||||
}
|
||||
|
||||
formatCost(value) {
|
||||
return (value || 0).toFixed(2);
|
||||
}
|
||||
|
||||
formatCostDetailed(value) {
|
||||
return (value || 0).toFixed(6);
|
||||
}
|
||||
|
||||
getStatusClass(status) {
|
||||
const classes = {
|
||||
success: "text-bg-success",
|
||||
error: "text-bg-danger",
|
||||
rate_limited: "text-bg-warning",
|
||||
budget_exceeded: "text-bg-warning",
|
||||
};
|
||||
return classes[status] || "text-bg-secondary";
|
||||
}
|
||||
|
||||
formatTime(isoString) {
|
||||
if (!isoString) return "";
|
||||
const d = new Date(isoString);
|
||||
return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
||||
}
|
||||
}
|
||||
|
||||
registry
|
||||
.category("actions")
|
||||
.add("fusion_api_dashboard", FusionApiDashboard);
|
||||
76
fusion_api/static/src/scss/fusion_api.scss
Normal file
76
fusion_api/static/src/scss/fusion_api.scss
Normal file
@@ -0,0 +1,76 @@
|
||||
.o_fusion_api_dashboard {
|
||||
background-color: var(--o-view-background-color);
|
||||
min-height: 100%;
|
||||
|
||||
.o_fusion_api_header {
|
||||
h2 {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.o_fusion_stat_card {
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.card-body h2 {
|
||||
font-weight: 700;
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.o_fusion_icon_circle {
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
min-width: 2.75rem;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 0.5rem;
|
||||
|
||||
.card-header {
|
||||
padding: 1rem 1.25rem 0.5rem;
|
||||
|
||||
h5 {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
|
||||
.table {
|
||||
th {
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
opacity: 0.65;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
td {
|
||||
vertical-align: middle;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
border-radius: 0.5rem;
|
||||
border-left-width: 4px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
padding: 0.35em 0.6em;
|
||||
}
|
||||
}
|
||||
228
fusion_api/static/src/xml/dashboard.xml
Normal file
228
fusion_api/static/src/xml/dashboard.xml
Normal file
@@ -0,0 +1,228 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="fusion_api.Dashboard">
|
||||
<div class="o_fusion_api_dashboard o_action">
|
||||
<!-- Header -->
|
||||
<div class="o_fusion_api_header d-flex align-items-center justify-content-between p-3 border-bottom bg-view">
|
||||
<h2 class="mb-0">Fusion API Dashboard</h2>
|
||||
<button class="btn btn-outline-primary" t-on-click="refresh">
|
||||
<i class="fa fa-refresh me-1"/> Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="o_fusion_api_content p-3" t-if="state.loaded">
|
||||
<!-- Stats Cards -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="card h-100 border-0 shadow-sm o_fusion_stat_card"
|
||||
style="cursor:pointer" t-on-click="openProviders">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="text-muted small text-uppercase">Active Providers</div>
|
||||
<h2 class="mb-0 mt-1" t-esc="state.activeProviders"/>
|
||||
</div>
|
||||
<div class="o_fusion_icon_circle bg-primary bg-opacity-25">
|
||||
<i class="fa fa-plug text-primary"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted small mt-2">
|
||||
<t t-esc="state.totalProviders"/> total configured
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="card h-100 border-0 shadow-sm o_fusion_stat_card"
|
||||
style="cursor:pointer" t-on-click="openConsumers">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="text-muted small text-uppercase">Active Modules</div>
|
||||
<h2 class="mb-0 mt-1" t-esc="state.activeConsumers"/>
|
||||
</div>
|
||||
<div class="o_fusion_icon_circle bg-success bg-opacity-25">
|
||||
<i class="fa fa-cubes text-success"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted small mt-2">
|
||||
<t t-esc="state.totalConsumers"/> total registered
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="card h-100 border-0 shadow-sm o_fusion_stat_card"
|
||||
style="cursor:pointer" t-on-click="openUsageLog">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="text-muted small text-uppercase">Month Cost</div>
|
||||
<h2 class="mb-0 mt-1">$<t t-esc="formatCost(state.monthCost)"/></h2>
|
||||
</div>
|
||||
<div class="o_fusion_icon_circle bg-warning bg-opacity-25">
|
||||
<i class="fa fa-usd text-warning"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted small mt-2">
|
||||
<t t-esc="state.monthRequests"/> requests this month
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="card h-100 border-0 shadow-sm o_fusion_stat_card"
|
||||
style="cursor:pointer" t-on-click="openUsageLog">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="text-muted small text-uppercase">Today</div>
|
||||
<h2 class="mb-0 mt-1" t-esc="state.todayRequests"/>
|
||||
</div>
|
||||
<div class="o_fusion_icon_circle bg-info bg-opacity-25">
|
||||
<i class="fa fa-bar-chart text-info"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted small mt-2">
|
||||
requests today
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Budget Alerts -->
|
||||
<div class="mb-4" t-if="state.approachingLimits.length > 0">
|
||||
<div class="alert alert-warning d-flex align-items-start" t-foreach="state.approachingLimits" t-as="alert" t-key="alert_index">
|
||||
<i class="fa fa-exclamation-triangle me-2 mt-1"/>
|
||||
<div>
|
||||
<strong t-esc="alert.consumer"/> on <t t-esc="alert.provider"/>:
|
||||
<t t-esc="alert.pct"/>% of budget used
|
||||
($<t t-esc="alert.spent"/> / $<t t-esc="alert.budget"/>)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<!-- Provider Stats -->
|
||||
<div class="col-lg-6" t-if="state.providerStats.length > 0">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-header bg-transparent border-bottom-0">
|
||||
<h5 class="mb-0">Provider Activity</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Provider</th>
|
||||
<th class="text-end">Keys</th>
|
||||
<th class="text-end">Requests</th>
|
||||
<th class="text-end">Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="state.providerStats" t-as="prov" t-key="prov.id">
|
||||
<td><strong t-esc="prov.name"/></td>
|
||||
<td class="text-end" t-esc="prov.keys"/>
|
||||
<td class="text-end" t-esc="prov.requests"/>
|
||||
<td class="text-end">$<t t-esc="formatCost(prov.cost)"/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Consumers -->
|
||||
<div class="col-lg-6" t-if="state.topConsumers.length > 0">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-header bg-transparent border-bottom-0">
|
||||
<h5 class="mb-0">Top Consumers (This Month)</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Module</th>
|
||||
<th class="text-end">Requests</th>
|
||||
<th class="text-end">Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="state.topConsumers" t-as="cons" t-key="cons_index">
|
||||
<td><strong t-esc="cons.name"/></td>
|
||||
<td class="text-end" t-esc="cons.requests"/>
|
||||
<td class="text-end">$<t t-esc="formatCost(cons.cost)"/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div class="mt-3" t-if="state.recentUsage.length > 0">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-transparent border-bottom-0 d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Recent Activity</h5>
|
||||
<button class="btn btn-sm btn-outline-secondary" t-on-click="openUsageLog">
|
||||
View All
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Consumer</th>
|
||||
<th>Provider</th>
|
||||
<th>Feature</th>
|
||||
<th class="text-end">Tokens</th>
|
||||
<th class="text-end">Cost</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="state.recentUsage" t-as="usage" t-key="usage_index">
|
||||
<td class="text-muted small" t-esc="formatTime(usage.time)"/>
|
||||
<td t-esc="usage.consumer"/>
|
||||
<td t-esc="usage.provider"/>
|
||||
<td class="text-muted" t-esc="usage.feature"/>
|
||||
<td class="text-end" t-esc="usage.tokens"/>
|
||||
<td class="text-end">$<t t-esc="formatCostDetailed(usage.cost)"/></td>
|
||||
<td>
|
||||
<span class="badge" t-att-class="getStatusClass(usage.status)"
|
||||
t-esc="usage.status"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div class="text-center py-5" t-if="state.monthRequests === 0 and state.activeProviders === 0">
|
||||
<i class="fa fa-rocket fa-3x text-muted mb-3 d-block"/>
|
||||
<h4>Welcome to Fusion API</h4>
|
||||
<p class="text-muted mb-3">
|
||||
Get started by configuring your API providers and adding your keys.
|
||||
</p>
|
||||
<button class="btn btn-primary" t-on-click="openProviders">
|
||||
<i class="fa fa-plus me-1"/> Configure Providers
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div class="text-center py-5" t-if="!state.loaded">
|
||||
<i class="fa fa-spinner fa-spin fa-2x text-muted"/>
|
||||
<p class="text-muted mt-2">Loading dashboard...</p>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
Reference in New Issue
Block a user