@littlebearapps/platform-admin-sdk 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -5
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +121 -3
- package/package.json +1 -1
- package/templates/full/dashboard/src/components/notifications/NotificationDropdown.tsx +130 -0
- package/templates/full/dashboard/src/components/notifications/NotificationItem.tsx +264 -0
- package/templates/full/dashboard/src/components/patterns/PatternInfoButton.tsx +60 -0
- package/templates/full/dashboard/src/components/reports/FeatureUsageReport.tsx +339 -0
- package/templates/full/dashboard/src/components/search/SearchResultGroup.tsx +46 -0
- package/templates/full/dashboard/src/components/search/SearchResultItem.tsx +212 -0
- package/templates/full/dashboard/src/pages/api/patterns/[id]/approve.ts +49 -0
- package/templates/full/dashboard/src/pages/api/patterns/[id]/reject.ts +50 -0
- package/templates/full/dashboard/src/pages/api/reports/digests/stats.ts +38 -0
- package/templates/full/dashboard/src/pages/api/reports/digests.ts +39 -0
- package/templates/full/dashboard/src/pages/api/search/reindex/[type].ts +56 -0
- package/templates/full/dashboard/src/pages/api/test-reports/[id].ts +102 -0
- package/templates/full/dashboard/src/pages/feedback.astro +365 -0
- package/templates/full/dashboard/src/pages/kiosk.astro +206 -0
- package/templates/full/dashboard/src/pages/map.astro +561 -0
- package/templates/full/dashboard/src/pages/revenue.astro +72 -0
- package/templates/full/dashboard/src/pages/tests.astro +431 -0
- package/templates/full/scripts/ops/audit-cost-anomaly.ts +430 -0
- package/templates/full/scripts/ops/verify-account-total.ts +256 -0
- package/templates/full/tests/integration/feedback-schema.test.ts +361 -0
- package/templates/full/tests/integration/r2-archive.test.ts +108 -0
- package/templates/shared/.github/workflows/dependabot-automerge.yml +41 -0
- package/templates/shared/.github/workflows/validate-controls.yml +27 -0
- package/templates/shared/dashboard/src/components/Breadcrumbs.astro +101 -0
- package/templates/shared/dashboard/src/components/EmptyState.astro +46 -0
- package/templates/shared/dashboard/src/components/ErrorBoundary.astro +79 -0
- package/templates/shared/dashboard/src/components/LoadingSkeleton.astro +105 -0
- package/templates/shared/dashboard/src/components/PageShell.astro +72 -0
- package/templates/shared/dashboard/src/components/SkipLinks.astro +22 -0
- package/templates/shared/dashboard/src/components/Toast.astro +170 -0
- package/templates/shared/dashboard/src/components/ToastContainer.astro +156 -0
- package/templates/shared/dashboard/src/components/costs/ProviderCostsGrid.tsx +401 -0
- package/templates/shared/dashboard/src/components/costs/index.ts +4 -0
- package/templates/shared/dashboard/src/components/overview/AlertBanner.tsx +94 -0
- package/templates/shared/dashboard/src/components/overview/index.ts +9 -0
- package/templates/shared/dashboard/src/components/resources/CostChart.tsx +170 -0
- package/templates/shared/dashboard/src/components/resources/ProviderCard.tsx +272 -0
- package/templates/shared/dashboard/src/components/resources/ProviderDetail.tsx +293 -0
- package/templates/shared/dashboard/src/components/settings/SettingsCard.astro +102 -0
- package/templates/shared/dashboard/src/components/usage/AllowanceGauge.astro +170 -0
- package/templates/shared/dashboard/src/components/usage/AnomalyAlerts.astro +633 -0
- package/templates/shared/dashboard/src/components/usage/BillingCycleCountdown.astro +192 -0
- package/templates/shared/dashboard/src/components/usage/BurnRateHero.astro +539 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerEventLog.astro +542 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerPanel.tsx +292 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerStatus.astro +669 -0
- package/templates/shared/dashboard/src/components/usage/CompactThresholdBanner.astro +531 -0
- package/templates/shared/dashboard/src/components/usage/ComparisonModeSelector.astro +651 -0
- package/templates/shared/dashboard/src/components/usage/CostBreakdownChart.astro +381 -0
- package/templates/shared/dashboard/src/components/usage/CostBreakdownTable.astro +210 -0
- package/templates/shared/dashboard/src/components/usage/CostDataTable.astro +0 -0
- package/templates/shared/dashboard/src/components/usage/CostDonutChart.astro +311 -0
- package/templates/shared/dashboard/src/components/usage/DailyCostChart.astro +632 -0
- package/templates/shared/dashboard/src/components/usage/ExportButton.astro +114 -0
- package/templates/shared/dashboard/src/components/usage/FeatureBudgetsTable.astro +872 -0
- package/templates/shared/dashboard/src/components/usage/FilterBar.astro +190 -0
- package/templates/shared/dashboard/src/components/usage/FilterToggles.astro +175 -0
- package/templates/shared/dashboard/src/components/usage/GitHubUsageCard.astro +537 -0
- package/templates/shared/dashboard/src/components/usage/OverageCostCard.astro +212 -0
- package/templates/shared/dashboard/src/components/usage/PlanUtilizationCard.astro +193 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCard.astro +640 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCardsGrid.astro +272 -0
- package/templates/shared/dashboard/src/components/usage/ResourceSearch.astro +279 -0
- package/templates/shared/dashboard/src/components/usage/ServiceUtilizationList.astro +604 -0
- package/templates/shared/dashboard/src/components/usage/SparklineCard.astro +399 -0
- package/templates/shared/dashboard/src/components/usage/StatsHero.astro +600 -0
- package/templates/shared/dashboard/src/components/usage/TableFilters.astro +1033 -0
- package/templates/shared/dashboard/src/components/usage/ThresholdAlert.astro +271 -0
- package/templates/shared/dashboard/src/components/usage/ThresholdSettings.astro +618 -0
- package/templates/shared/dashboard/src/components/usage/TopSpenderCard.astro +170 -0
- package/templates/shared/dashboard/src/components/usage/UnifiedResourceTable.astro +1737 -0
- package/templates/shared/dashboard/src/components/usage/UsageCard.astro +135 -0
- package/templates/shared/dashboard/src/components/usage/UsageHealthBanner.astro +387 -0
- package/templates/shared/dashboard/src/components/usage/UtilizationBar.astro +159 -0
- package/templates/shared/dashboard/src/components/usage/WorkersBreakdownTable.astro +659 -0
- package/templates/shared/dashboard/src/components/usage/daily/CostChart.astro +461 -0
- package/templates/shared/dashboard/src/components/usage/daily/CostTable.astro +946 -0
- package/templates/shared/dashboard/src/components/usage/daily/DailyOverview.astro +1079 -0
- package/templates/shared/dashboard/src/components/usage/design-tokens.ts +187 -0
- package/templates/shared/dashboard/src/components/usage/filters/InlineDateRange.astro +285 -0
- package/templates/shared/dashboard/src/components/usage/filters/PeriodButtons.astro +157 -0
- package/templates/shared/dashboard/src/components/usage/filters/ProjectSelect.astro +284 -0
- package/templates/shared/dashboard/src/components/usage/scripts/ai-tab-controller.ts +419 -0
- package/templates/shared/dashboard/src/components/usage/scripts/constants.ts +60 -0
- package/templates/shared/dashboard/src/components/usage/scripts/formatters.ts +62 -0
- package/templates/shared/dashboard/src/components/usage/scripts/overview-controller.ts +1633 -0
- package/templates/shared/dashboard/src/components/usage/scripts/resource-table-builder.ts +294 -0
- package/templates/shared/dashboard/src/components/usage/scripts/tabs-filters-controller.ts +464 -0
- package/templates/shared/dashboard/src/components/usage/state/index.ts +55 -0
- package/templates/shared/dashboard/src/components/usage/state/usageActions.ts +439 -0
- package/templates/shared/dashboard/src/components/usage/state/usageStore.ts +376 -0
- package/templates/shared/dashboard/src/components/usage/types.ts +283 -0
- package/templates/shared/dashboard/src/components/usage/usage-colors.ts +292 -0
- package/templates/shared/dashboard/src/pages/api/usage/ai-models.ts +235 -0
- package/templates/shared/dashboard/src/pages/api/usage/billing-context.ts +296 -0
- package/templates/shared/scripts/test-telemetry-flow.ts +464 -0
- package/templates/shared/tests/e2e/usage-export.test.ts +784 -0
- package/templates/shared/tests/e2e/usage-mobile.test.ts +531 -0
- package/templates/standard/dashboard/src/components/errors/PriorityBadge.astro +27 -0
- package/templates/standard/dashboard/src/components/infrastructure/HealthchecksStatus.tsx +293 -0
- package/templates/standard/dashboard/src/components/infrastructure/InfrastructureTabs.tsx +268 -0
- package/templates/standard/dashboard/src/pages/analytics.astro +64 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/alerts.ts +85 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks/[id]/flips.ts +110 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks.ts +101 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/uptime/[id]/response-times.ts +121 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/uptime.ts +89 -0
- package/templates/standard/dashboard/src/pages/api/test/service-auth.ts +178 -0
- package/templates/standard/tests/integration/connectors.test.ts +241 -0
- package/templates/standard/tests/integration/github-monitor.test.ts +143 -0
- package/templates/standard/tests/integration/ingestion.test.ts +211 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CircuitBreakerPanel — Circuit breaker status at all 3 levels with manual toggles
|
|
3
|
+
*
|
|
4
|
+
* Shows global, per-project, and recently-tripped feature breaker state.
|
|
5
|
+
* Fetches GET /api/usage/circuit-breakers and POSTs trip/reset actions.
|
|
6
|
+
*/
|
|
7
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// TYPES
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
interface BreakerState {
|
|
14
|
+
level: 'global' | 'project' | 'feature';
|
|
15
|
+
key: string;
|
|
16
|
+
label: string;
|
|
17
|
+
status: 'active' | 'stopped';
|
|
18
|
+
reason?: string;
|
|
19
|
+
integrated?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface CircuitBreakerData {
|
|
23
|
+
success: true;
|
|
24
|
+
breakers: BreakerState[];
|
|
25
|
+
recentlyTripped: BreakerState[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// COMPONENT
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
export default function CircuitBreakerPanel() {
|
|
33
|
+
const [data, setData] = useState<CircuitBreakerData | null>(null);
|
|
34
|
+
const [error, setError] = useState<string | null>(null);
|
|
35
|
+
const [loading, setLoading] = useState(true);
|
|
36
|
+
const [pendingAction, setPendingAction] = useState<string | null>(null);
|
|
37
|
+
const [confirm, setConfirm] = useState<{ level: string; key: string; action: 'trip' | 'reset'; label: string } | null>(null);
|
|
38
|
+
|
|
39
|
+
const fetchData = useCallback(() => {
|
|
40
|
+
fetch('/api/usage/circuit-breakers')
|
|
41
|
+
.then((res) => res.json())
|
|
42
|
+
.then((json) => {
|
|
43
|
+
if (json.success) {
|
|
44
|
+
setData(json as CircuitBreakerData);
|
|
45
|
+
} else {
|
|
46
|
+
setError(json.error ?? 'Unknown error');
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
.catch((err) => setError(err.message))
|
|
50
|
+
.finally(() => setLoading(false));
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
fetchData();
|
|
55
|
+
}, [fetchData]);
|
|
56
|
+
|
|
57
|
+
const handleToggle = async (level: string, key: string, action: 'trip' | 'reset'): Promise<void> => {
|
|
58
|
+
const actionKey = `${level}:${key}`;
|
|
59
|
+
setPendingAction(actionKey);
|
|
60
|
+
setConfirm(null);
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch('/api/usage/circuit-breakers', {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: { 'Content-Type': 'application/json' },
|
|
66
|
+
body: JSON.stringify({ level, key, action }),
|
|
67
|
+
});
|
|
68
|
+
const json = await res.json();
|
|
69
|
+
if (!json.success) {
|
|
70
|
+
setError(json.error ?? 'Toggle failed');
|
|
71
|
+
} else {
|
|
72
|
+
// Refresh state
|
|
73
|
+
fetchData();
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
setError(err instanceof Error ? err.message : 'Toggle failed');
|
|
77
|
+
} finally {
|
|
78
|
+
setPendingAction(null);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const requestToggle = (breaker: BreakerState): void => {
|
|
83
|
+
const action = breaker.status === 'active' ? 'trip' : 'reset';
|
|
84
|
+
setConfirm({ level: breaker.level, key: breaker.key, action, label: breaker.label });
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (loading) {
|
|
88
|
+
return (
|
|
89
|
+
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 shadow-sm">
|
|
90
|
+
<div className="animate-pulse space-y-3">
|
|
91
|
+
<div className="h-5 bg-gray-200 dark:bg-gray-700 rounded w-1/3" />
|
|
92
|
+
<div className="space-y-2">
|
|
93
|
+
{[...Array(4)].map((_, i) => (
|
|
94
|
+
<div key={i} className="h-8 bg-gray-200 dark:bg-gray-700 rounded" />
|
|
95
|
+
))}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (error && !data) {
|
|
103
|
+
return (
|
|
104
|
+
<div className="bg-white dark:bg-gray-800 border border-red-200 dark:border-red-800 rounded-lg p-4 shadow-sm">
|
|
105
|
+
<p className="text-sm text-red-600 dark:text-red-400">Failed to load circuit breakers: {error}</p>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!data) return null;
|
|
111
|
+
|
|
112
|
+
const globalBreaker = data.breakers.find((b) => b.level === 'global');
|
|
113
|
+
const projectBreakers = data.breakers.filter((b) => b.level === 'project');
|
|
114
|
+
const featureBreakers = data.recentlyTripped;
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 shadow-sm">
|
|
118
|
+
{/* Header */}
|
|
119
|
+
<div className="flex items-center justify-between mb-4">
|
|
120
|
+
<h3 className="text-base font-semibold text-gray-900 dark:text-gray-100">
|
|
121
|
+
Circuit Breakers
|
|
122
|
+
</h3>
|
|
123
|
+
<button
|
|
124
|
+
onClick={fetchData}
|
|
125
|
+
className="text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
|
126
|
+
title="Refresh"
|
|
127
|
+
>
|
|
128
|
+
Refresh
|
|
129
|
+
</button>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{/* Confirmation Dialog */}
|
|
133
|
+
{confirm && (
|
|
134
|
+
<div className="mb-4 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
|
|
135
|
+
<p className="text-sm text-amber-800 dark:text-amber-200 mb-2">
|
|
136
|
+
{confirm.action === 'trip'
|
|
137
|
+
? `Stop all traffic for "${confirm.label}"?`
|
|
138
|
+
: `Re-enable traffic for "${confirm.label}"?`}
|
|
139
|
+
</p>
|
|
140
|
+
<div className="flex gap-2">
|
|
141
|
+
<button
|
|
142
|
+
onClick={() => handleToggle(confirm.level, confirm.key, confirm.action)}
|
|
143
|
+
className={`px-3 py-1 text-xs font-medium rounded ${
|
|
144
|
+
confirm.action === 'trip'
|
|
145
|
+
? 'bg-red-600 text-white hover:bg-red-700'
|
|
146
|
+
: 'bg-emerald-600 text-white hover:bg-emerald-700'
|
|
147
|
+
} transition-colors`}
|
|
148
|
+
>
|
|
149
|
+
{confirm.action === 'trip' ? 'Confirm Stop' : 'Confirm Reset'}
|
|
150
|
+
</button>
|
|
151
|
+
<button
|
|
152
|
+
onClick={() => setConfirm(null)}
|
|
153
|
+
className="px-3 py-1 text-xs font-medium text-gray-600 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
|
154
|
+
>
|
|
155
|
+
Cancel
|
|
156
|
+
</button>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
|
|
161
|
+
{/* Error banner (non-fatal) */}
|
|
162
|
+
{error && data && (
|
|
163
|
+
<div className="mb-3 p-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded text-xs text-red-600 dark:text-red-400">
|
|
164
|
+
{error}
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
<div className="space-y-4">
|
|
169
|
+
{/* Global */}
|
|
170
|
+
{globalBreaker && (
|
|
171
|
+
<BreakerRow
|
|
172
|
+
breaker={globalBreaker}
|
|
173
|
+
icon="globe"
|
|
174
|
+
onToggle={requestToggle}
|
|
175
|
+
isPending={pendingAction === `global:${globalBreaker.key}`}
|
|
176
|
+
/>
|
|
177
|
+
)}
|
|
178
|
+
|
|
179
|
+
{/* Projects */}
|
|
180
|
+
<div>
|
|
181
|
+
<p className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">
|
|
182
|
+
Projects
|
|
183
|
+
</p>
|
|
184
|
+
<div className="space-y-1.5">
|
|
185
|
+
{projectBreakers.map((b) => (
|
|
186
|
+
<BreakerRow
|
|
187
|
+
key={b.key}
|
|
188
|
+
breaker={b}
|
|
189
|
+
icon="project"
|
|
190
|
+
onToggle={requestToggle}
|
|
191
|
+
isPending={pendingAction === `project:${b.key}`}
|
|
192
|
+
/>
|
|
193
|
+
))}
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
{/* Recently Tripped Features */}
|
|
198
|
+
<div>
|
|
199
|
+
<p className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">
|
|
200
|
+
Recently Tripped Features (24h)
|
|
201
|
+
</p>
|
|
202
|
+
{featureBreakers.length === 0 ? (
|
|
203
|
+
<p className="text-xs text-gray-400 dark:text-gray-500 italic">None</p>
|
|
204
|
+
) : (
|
|
205
|
+
<div className="space-y-1.5">
|
|
206
|
+
{featureBreakers.map((b) => (
|
|
207
|
+
<BreakerRow
|
|
208
|
+
key={b.key}
|
|
209
|
+
breaker={b}
|
|
210
|
+
icon="feature"
|
|
211
|
+
onToggle={requestToggle}
|
|
212
|
+
isPending={pendingAction === `feature:${b.key}`}
|
|
213
|
+
/>
|
|
214
|
+
))}
|
|
215
|
+
</div>
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// =============================================================================
|
|
224
|
+
// SUB-COMPONENT: BreakerRow
|
|
225
|
+
// =============================================================================
|
|
226
|
+
|
|
227
|
+
interface BreakerRowProps {
|
|
228
|
+
breaker: BreakerState;
|
|
229
|
+
icon: 'globe' | 'project' | 'feature';
|
|
230
|
+
onToggle: (breaker: BreakerState) => void;
|
|
231
|
+
isPending: boolean;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const ICONS: Record<string, string> = {
|
|
235
|
+
globe: '\uD83C\uDF10',
|
|
236
|
+
project: '\uD83D\uDCE6',
|
|
237
|
+
feature: '\u26A1',
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
function BreakerRow({ breaker, icon, onToggle, isPending }: BreakerRowProps) {
|
|
241
|
+
const isActive = breaker.status === 'active';
|
|
242
|
+
const isNotIntegrated = breaker.level === 'project' && breaker.integrated === false;
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<div className={`flex items-center justify-between py-1.5 px-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors ${isNotIntegrated ? 'opacity-50' : ''}`}>
|
|
246
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
247
|
+
<span className="text-sm flex-shrink-0">{ICONS[icon]}</span>
|
|
248
|
+
<span className={`text-sm truncate ${isNotIntegrated ? 'text-gray-400 dark:text-gray-500' : 'text-gray-700 dark:text-gray-300'}`}>
|
|
249
|
+
{breaker.label}
|
|
250
|
+
</span>
|
|
251
|
+
{isNotIntegrated && (
|
|
252
|
+
<span className="text-[10px] text-gray-400 dark:text-gray-500 flex-shrink-0">(no SDK)</span>
|
|
253
|
+
)}
|
|
254
|
+
{breaker.reason && (
|
|
255
|
+
<span className="text-xs text-gray-400 dark:text-gray-500 truncate" title={breaker.reason}>
|
|
256
|
+
({breaker.reason})
|
|
257
|
+
</span>
|
|
258
|
+
)}
|
|
259
|
+
</div>
|
|
260
|
+
<div className="flex items-center gap-2 flex-shrink-0 ml-2">
|
|
261
|
+
{isNotIntegrated ? (
|
|
262
|
+
<span className="inline-flex items-center gap-1 text-xs text-gray-400 dark:text-gray-500">
|
|
263
|
+
<span className="w-1.5 h-1.5 rounded-full bg-gray-300 dark:bg-gray-600" />
|
|
264
|
+
N/A
|
|
265
|
+
</span>
|
|
266
|
+
) : (
|
|
267
|
+
<span className={`inline-flex items-center gap-1 text-xs font-medium ${
|
|
268
|
+
isActive
|
|
269
|
+
? 'text-emerald-600 dark:text-emerald-400'
|
|
270
|
+
: 'text-red-600 dark:text-red-400'
|
|
271
|
+
}`}>
|
|
272
|
+
<span className={`w-1.5 h-1.5 rounded-full ${isActive ? 'bg-emerald-500' : 'bg-red-500'}`} />
|
|
273
|
+
{isActive ? 'Active' : 'Stopped'}
|
|
274
|
+
</span>
|
|
275
|
+
)}
|
|
276
|
+
<button
|
|
277
|
+
onClick={() => onToggle(breaker)}
|
|
278
|
+
disabled={isPending}
|
|
279
|
+
className={`px-2 py-0.5 text-xs rounded border transition-colors ${
|
|
280
|
+
isPending
|
|
281
|
+
? 'opacity-50 cursor-wait'
|
|
282
|
+
: isActive
|
|
283
|
+
? 'text-red-600 border-red-200 hover:bg-red-50 dark:text-red-400 dark:border-red-800 dark:hover:bg-red-900/20'
|
|
284
|
+
: 'text-emerald-600 border-emerald-200 hover:bg-emerald-50 dark:text-emerald-400 dark:border-emerald-800 dark:hover:bg-emerald-900/20'
|
|
285
|
+
}`}
|
|
286
|
+
>
|
|
287
|
+
{isPending ? '...' : isActive ? 'Trip' : 'Reset'}
|
|
288
|
+
</button>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
}
|