@escalated-dev/escalated 0.7.0 → 0.8.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/LICENSE +21 -21
- package/README.md +36 -1
- package/package.json +16 -7
- package/src/components/ActiveChatsPanel.vue +246 -0
- package/src/components/ActivityTimeline.stories.js +55 -55
- package/src/components/ActivityTimeline.vue +83 -83
- package/src/components/AdminDashboard.stories.js +1423 -1095
- package/src/components/AgentLoadIndicator.stories.js +37 -37
- package/src/components/AgentLoadIndicator.vue +49 -49
- package/src/components/AgentPerformanceCard.vue +44 -44
- package/src/components/ArticleEditor.vue +82 -82
- package/src/components/AssigneeSelect.vue +37 -37
- package/src/components/AttachmentList.vue +13 -1
- package/src/components/AuditLogEntry.vue +95 -95
- package/src/components/BulkActionBar.vue +166 -166
- package/src/components/CategoryTree.vue +89 -89
- package/src/components/ChartWidget.vue +46 -46
- package/src/components/ChatActionBar.vue +226 -0
- package/src/components/ChatBubble.vue +162 -0
- package/src/components/ChatComposer.vue +308 -0
- package/src/components/ChatQueue.vue +222 -0
- package/src/components/ChatThread.vue +197 -0
- package/src/components/CollisionWarning.stories.js +30 -30
- package/src/components/CollisionWarning.vue +68 -68
- package/src/components/ConditionalFieldRules.vue +135 -135
- package/src/components/ContextPanel.vue +66 -66
- package/src/components/ContextPanelSection.vue +65 -65
- package/src/components/CustomFieldRenderer.vue +159 -159
- package/src/components/EscalatedLayout.vue +35 -6
- package/src/components/FileDropzone.vue +57 -57
- package/src/components/FollowButton.stories.js +30 -30
- package/src/components/FollowButton.vue +88 -88
- package/src/components/KeyboardShortcutHelp.vue +213 -213
- package/src/components/KnowledgePanel.vue +122 -122
- package/src/components/KpiCard.stories.js +32 -32
- package/src/components/KpiCard.vue +88 -88
- package/src/components/MacroDropdown.vue +150 -150
- package/src/components/MentionDropdown.vue +154 -0
- package/src/components/MetricComparison.vue +89 -0
- package/src/components/PercentileChart.vue +67 -0
- package/src/components/PeriodSelector.vue +98 -0
- package/src/components/PermissionMatrix.vue +107 -107
- package/src/components/PinnedNotes.vue +129 -129
- package/src/components/PluginSlot.vue +68 -62
- package/src/components/PresenceIndicator.vue +46 -2
- package/src/components/PriorityBadge.stories.js +33 -33
- package/src/components/PriorityBadge.vue +46 -46
- package/src/components/ReplyComposer.vue +81 -18
- package/src/components/ReplyThread.vue +40 -2
- package/src/components/ReportChart.vue +395 -0
- package/src/components/ReportExportButton.vue +108 -0
- package/src/components/SatisfactionRating.vue +180 -180
- package/src/components/SaveViewDialog.vue +172 -0
- package/src/components/SavedViewSidebar.vue +208 -0
- package/src/components/ScheduleEditor.vue +104 -104
- package/src/components/SideConversation.vue +165 -165
- package/src/components/SideConversationList.vue +147 -147
- package/src/components/SkillTagManager.vue +83 -79
- package/src/components/SlaComplianceChart.vue +46 -53
- package/src/components/SlaTimer.stories.js +35 -35
- package/src/components/SlaTimer.vue +62 -62
- package/src/components/SnoozeButton.vue +247 -0
- package/src/components/StatsCard.stories.js +24 -24
- package/src/components/StatsCard.vue +58 -58
- package/src/components/StatusBadge.stories.js +53 -53
- package/src/components/StatusBadge.vue +78 -78
- package/src/components/TagSelect.vue +84 -84
- package/src/components/TicketFilters.vue +270 -225
- package/src/components/TicketLinkPanel.vue +193 -193
- package/src/components/TicketList.vue +655 -594
- package/src/components/TicketMergeDialog.vue +157 -157
- package/src/components/TicketSplitDialog.vue +98 -0
- package/src/components/TotpInput.stories.js +9 -9
- package/src/components/TotpInput.vue +81 -81
- package/src/components/TwoFactorSetup.vue +113 -113
- package/src/components/VariableTokenInput.vue +111 -0
- package/src/components/WorkflowActionConfig.vue +287 -0
- package/src/components/WorkflowActionList.vue +350 -0
- package/src/components/WorkflowConditionBuilder.vue +265 -0
- package/src/components/WorkflowTriggerSelector.vue +125 -0
- package/src/composables/useChat.js +187 -0
- package/src/composables/useI18n.js +30 -8
- package/src/composables/useKeyboardShortcuts.js +46 -46
- package/src/composables/useMentions.js +134 -0
- package/src/composables/usePluginExtensions.js +159 -159
- package/src/composables/usePluginHooks.js +65 -65
- package/src/composables/useRealtime.js +182 -0
- package/src/defineEscalatedPlugin.js +76 -76
- package/src/index.js +84 -72
- package/src/locales/README.md +13 -0
- package/src/locales/ar.json +727 -0
- package/src/locales/de.json +678 -430
- package/src/locales/en.json +717 -455
- package/src/locales/es.json +679 -430
- package/src/locales/fr.json +680 -431
- package/src/locales/index.js +12 -2
- package/src/locales/it.json +727 -0
- package/src/locales/ja.json +727 -0
- package/src/locales/ko.json +727 -0
- package/src/locales/nl.json +727 -0
- package/src/locales/pl.json +727 -0
- package/src/locales/pt-BR.json +727 -0
- package/src/locales/ru.json +727 -0
- package/src/locales/tr.json +727 -0
- package/src/locales/zh-CN.json +727 -0
- package/src/pages/Admin/ApiTokens/Index.vue +381 -381
- package/src/pages/Admin/AuditLog/Index.vue +154 -154
- package/src/pages/Admin/BusinessHours/Form.vue +172 -172
- package/src/pages/Admin/BusinessHours/Index.vue +113 -113
- package/src/pages/Admin/CannedResponses/Index.vue +127 -127
- package/src/pages/Admin/Capacity/Index.vue +134 -134
- package/src/pages/Admin/CustomFields/Form.vue +195 -195
- package/src/pages/Admin/CustomFields/Index.vue +134 -134
- package/src/pages/Admin/CustomObjects/Form.vue +202 -202
- package/src/pages/Admin/CustomObjects/Index.vue +127 -127
- package/src/pages/Admin/CustomObjects/Records.vue +228 -228
- package/src/pages/Admin/Departments/Form.vue +75 -75
- package/src/pages/Admin/Departments/Index.vue +110 -109
- package/src/pages/Admin/EscalationRules/Form.vue +157 -157
- package/src/pages/Admin/EscalationRules/Index.vue +178 -178
- package/src/pages/Admin/Import/Connect.vue +0 -1
- package/src/pages/Admin/Import/Index.vue +1 -1
- package/src/pages/Admin/Import/Progress.vue +1 -1
- package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -142
- package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -195
- package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -193
- package/src/pages/Admin/Macros/Index.vue +448 -448
- package/src/pages/Admin/Plugins/Index.vue +327 -327
- package/src/pages/Admin/Reports/AgentDetail.vue +132 -0
- package/src/pages/Admin/Reports/AgentMetrics.vue +115 -115
- package/src/pages/Admin/Reports/AgentRanking.vue +246 -0
- package/src/pages/Admin/Reports/Cohorts.vue +180 -0
- package/src/pages/Admin/Reports/Comparison.vue +141 -0
- package/src/pages/Admin/Reports/CsatReport.vue +127 -127
- package/src/pages/Admin/Reports/Dashboard.vue +36 -21
- package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -0
- package/src/pages/Admin/Reports/ResponseTimes.vue +164 -0
- package/src/pages/Admin/Reports/SlaReport.vue +162 -166
- package/src/pages/Admin/Reports/SlaTrends.vue +183 -0
- package/src/pages/Admin/Reports.vue +206 -206
- package/src/pages/Admin/Roles/Form.vue +71 -71
- package/src/pages/Admin/Roles/Index.vue +122 -122
- package/src/pages/Admin/Settings/CsatSettings.vue +165 -165
- package/src/pages/Admin/Settings/DataRetention.vue +182 -182
- package/src/pages/Admin/Settings/EmailSettings.vue +243 -243
- package/src/pages/Admin/Settings/Sandbox.vue +135 -135
- package/src/pages/Admin/Settings/SsoSettings.vue +218 -218
- package/src/pages/Admin/Settings/TwoFactor.vue +110 -110
- package/src/pages/Admin/Settings.vue +1008 -468
- package/src/pages/Admin/Skills/Form.vue +193 -53
- package/src/pages/Admin/Skills/Index.vue +105 -94
- package/src/pages/Admin/SlaPolicies/Form.vue +118 -118
- package/src/pages/Admin/SlaPolicies/Index.vue +121 -120
- package/src/pages/Admin/Statuses/Form.vue +114 -114
- package/src/pages/Admin/Statuses/Index.vue +153 -153
- package/src/pages/Admin/Tags/Index.vue +191 -191
- package/src/pages/Admin/Tickets/Index.vue +126 -91
- package/src/pages/Admin/Tickets/Show.vue +29 -0
- package/src/pages/Admin/Users/Index.vue +145 -0
- package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -120
- package/src/pages/Admin/Webhooks/Form.vue +110 -110
- package/src/pages/Admin/Webhooks/Index.vue +136 -136
- package/src/pages/Admin/Workflows/Builder.vue +207 -0
- package/src/pages/Admin/Workflows/Index.vue +235 -0
- package/src/pages/Admin/Workflows/Logs.vue +286 -0
- package/src/pages/Agent/TicketIndex.vue +112 -88
- package/src/pages/Agent/TicketShow.vue +192 -60
- package/src/pages/Customer/Create.vue +1 -1
- package/src/pages/Customer/Index.vue +26 -1
- package/src/pages/Customer/KnowledgeBase/Article.vue +4 -2
- package/src/pages/Customer/KnowledgeBase/Index.vue +151 -151
- package/src/pages/Customer/Show.vue +69 -63
- package/src/pages/Guest/Create.vue +40 -15
- package/src/pages/Guest/Show.vue +124 -93
- package/src/plugin.js +26 -1
- package/src/utils/mentionHighlight.js +24 -0
- package/src/utils/sanitizeHtml.js +78 -64
- package/src/widget/EscalatedWidget.vue +804 -0
- package/src/widget/index.js +254 -0
- package/src/pages/Admin/Automations/Form.vue +0 -184
- package/src/pages/Admin/Automations/Index.vue +0 -129
|
@@ -1,381 +1,381 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { ref, computed, inject } from 'vue';
|
|
3
|
-
import { useForm, usePage } from '@inertiajs/vue3';
|
|
4
|
-
import EscalatedLayout from '../../../components/EscalatedLayout.vue';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
tokens: Array,
|
|
8
|
-
users: Array,
|
|
9
|
-
api_enabled: Boolean,
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
const dark = inject(
|
|
13
|
-
'esc-dark',
|
|
14
|
-
computed(() => true),
|
|
15
|
-
);
|
|
16
|
-
const page = usePage();
|
|
17
|
-
const prefix = computed(() => {
|
|
18
|
-
const p = page.props.escalated?.prefix || 'support';
|
|
19
|
-
return p.startsWith('/') ? p : `/${p}`;
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
const showCreate = ref(false);
|
|
23
|
-
const copiedToken = ref(null);
|
|
24
|
-
|
|
25
|
-
const form = useForm({
|
|
26
|
-
name: '',
|
|
27
|
-
user_id: '',
|
|
28
|
-
abilities: ['agent'],
|
|
29
|
-
expires_in_days: null,
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
function createToken() {
|
|
33
|
-
form.post(`${prefix.value}/admin/api-tokens`, {
|
|
34
|
-
preserveScroll: true,
|
|
35
|
-
onSuccess: (page) => {
|
|
36
|
-
const flash = page.props.flash || {};
|
|
37
|
-
if (flash.plain_text_token) {
|
|
38
|
-
copiedToken.value = flash.plain_text_token;
|
|
39
|
-
}
|
|
40
|
-
form.reset();
|
|
41
|
-
showCreate.value = false;
|
|
42
|
-
},
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function revokeToken(id) {
|
|
47
|
-
if (!confirm('Are you sure you want to revoke this token? This cannot be undone.')) return;
|
|
48
|
-
useForm({}).delete(`${prefix.value}/admin/api-tokens/${id}`, { preserveScroll: true });
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function copyToken() {
|
|
52
|
-
if (copiedToken.value) {
|
|
53
|
-
navigator.clipboard.writeText(copiedToken.value);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function dismissToken() {
|
|
58
|
-
copiedToken.value = null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function toggleAbility(ability) {
|
|
62
|
-
const idx = form.abilities.indexOf(ability);
|
|
63
|
-
if (idx >= 0) {
|
|
64
|
-
form.abilities.splice(idx, 1);
|
|
65
|
-
} else {
|
|
66
|
-
form.abilities.push(ability);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function formatDate(iso) {
|
|
71
|
-
if (!iso) return '—';
|
|
72
|
-
return new Date(iso).toLocaleDateString(undefined, {
|
|
73
|
-
year: 'numeric',
|
|
74
|
-
month: 'short',
|
|
75
|
-
day: 'numeric',
|
|
76
|
-
hour: '2-digit',
|
|
77
|
-
minute: '2-digit',
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
</script>
|
|
81
|
-
|
|
82
|
-
<template>
|
|
83
|
-
<EscalatedLayout title="API Tokens">
|
|
84
|
-
<div class="space-y-6">
|
|
85
|
-
<!-- Header -->
|
|
86
|
-
<div class="flex items-center justify-between">
|
|
87
|
-
<div>
|
|
88
|
-
<h2 :class="dark ? 'text-[var(--esc-panel-text)]' : 'text-gray-900'" class="text-lg font-semibold">
|
|
89
|
-
API Tokens
|
|
90
|
-
</h2>
|
|
91
|
-
<p :class="dark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500'" class="mt-1 text-sm">
|
|
92
|
-
Manage API tokens for desktop apps and external integrations.
|
|
93
|
-
</p>
|
|
94
|
-
</div>
|
|
95
|
-
<button
|
|
96
|
-
:class="
|
|
97
|
-
dark ? 'bg-white text-black hover:bg-neutral-200' : 'bg-gray-900 text-white hover:bg-gray-800'
|
|
98
|
-
"
|
|
99
|
-
class="rounded-lg px-4 py-2 text-sm font-medium transition-colors"
|
|
100
|
-
@click="showCreate = true"
|
|
101
|
-
>
|
|
102
|
-
Create Token
|
|
103
|
-
</button>
|
|
104
|
-
</div>
|
|
105
|
-
|
|
106
|
-
<!-- Not enabled warning -->
|
|
107
|
-
<div
|
|
108
|
-
v-if="!api_enabled"
|
|
109
|
-
:class="
|
|
110
|
-
dark
|
|
111
|
-
? 'border-amber-500/30 bg-amber-500/10 text-amber-300'
|
|
112
|
-
: 'border-amber-300 bg-amber-50 text-amber-800'
|
|
113
|
-
"
|
|
114
|
-
class="rounded-lg border p-4 text-sm"
|
|
115
|
-
>
|
|
116
|
-
The REST API is currently disabled. Set <code class="font-mono">ESCALATED_API_ENABLED=true</code> in
|
|
117
|
-
your .env to enable it.
|
|
118
|
-
</div>
|
|
119
|
-
|
|
120
|
-
<!-- Copied token modal -->
|
|
121
|
-
<div
|
|
122
|
-
v-if="copiedToken"
|
|
123
|
-
:class="dark ? 'border-emerald-500/30 bg-emerald-500/10' : 'border-emerald-300 bg-emerald-50'"
|
|
124
|
-
class="rounded-lg border p-4"
|
|
125
|
-
>
|
|
126
|
-
<p :class="dark ? 'text-emerald-300' : 'text-emerald-800'" class="text-sm font-medium">
|
|
127
|
-
Token created! Copy it now — it won't be shown again.
|
|
128
|
-
</p>
|
|
129
|
-
<div class="mt-2 flex items-center gap-2">
|
|
130
|
-
<code
|
|
131
|
-
:class="dark ? 'bg-black text-emerald-400' : 'bg-white text-emerald-700'"
|
|
132
|
-
class="flex-1 rounded border px-3 py-2 text-xs font-mono break-all"
|
|
133
|
-
:style="dark ? 'border-color: rgba(255,255,255,0.1)' : ''"
|
|
134
|
-
>{{ copiedToken }}</code
|
|
135
|
-
>
|
|
136
|
-
<button
|
|
137
|
-
:class="
|
|
138
|
-
dark
|
|
139
|
-
? 'bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text)] hover:bg-[var(--esc-panel-active)]'
|
|
140
|
-
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
|
141
|
-
"
|
|
142
|
-
class="rounded-lg px-3 py-2 text-sm font-medium transition-colors"
|
|
143
|
-
@click="copyToken"
|
|
144
|
-
>
|
|
145
|
-
Copy
|
|
146
|
-
</button>
|
|
147
|
-
<button
|
|
148
|
-
:class="
|
|
149
|
-
dark
|
|
150
|
-
? 'text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text)]'
|
|
151
|
-
: 'text-gray-400 hover:text-gray-600'
|
|
152
|
-
"
|
|
153
|
-
class="px-2 py-2 text-sm transition-colors"
|
|
154
|
-
@click="dismissToken"
|
|
155
|
-
>
|
|
156
|
-
Dismiss
|
|
157
|
-
</button>
|
|
158
|
-
</div>
|
|
159
|
-
</div>
|
|
160
|
-
|
|
161
|
-
<!-- Create form -->
|
|
162
|
-
<div
|
|
163
|
-
v-if="showCreate"
|
|
164
|
-
:class="
|
|
165
|
-
dark
|
|
166
|
-
? 'border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface-alt)]'
|
|
167
|
-
: 'border-gray-200 bg-white'
|
|
168
|
-
"
|
|
169
|
-
class="rounded-lg border p-5 space-y-4"
|
|
170
|
-
>
|
|
171
|
-
<h3 :class="dark ? 'text-[var(--esc-panel-text)]' : 'text-gray-900'" class="text-sm font-semibold">
|
|
172
|
-
New API Token
|
|
173
|
-
</h3>
|
|
174
|
-
|
|
175
|
-
<div class="grid grid-cols-2 gap-4">
|
|
176
|
-
<div>
|
|
177
|
-
<label
|
|
178
|
-
:class="dark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600'"
|
|
179
|
-
class="mb-1 block text-xs font-medium"
|
|
180
|
-
>Token Name</label
|
|
181
|
-
>
|
|
182
|
-
<input
|
|
183
|
-
v-model="form.name"
|
|
184
|
-
type="text"
|
|
185
|
-
placeholder="e.g. Desktop App - Work Laptop"
|
|
186
|
-
:class="
|
|
187
|
-
dark
|
|
188
|
-
? 'border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text)]'
|
|
189
|
-
: 'border-gray-300 bg-white text-gray-900'
|
|
190
|
-
"
|
|
191
|
-
class="w-full rounded-lg border px-3 py-2 text-sm"
|
|
192
|
-
/>
|
|
193
|
-
</div>
|
|
194
|
-
<div>
|
|
195
|
-
<label
|
|
196
|
-
:class="dark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600'"
|
|
197
|
-
class="mb-1 block text-xs font-medium"
|
|
198
|
-
>User</label
|
|
199
|
-
>
|
|
200
|
-
<select
|
|
201
|
-
v-model="form.user_id"
|
|
202
|
-
:class="
|
|
203
|
-
dark
|
|
204
|
-
? 'border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text)]'
|
|
205
|
-
: 'border-gray-300 bg-white text-gray-900'
|
|
206
|
-
"
|
|
207
|
-
class="w-full rounded-lg border px-3 py-2 text-sm"
|
|
208
|
-
>
|
|
209
|
-
<option value="">Select user...</option>
|
|
210
|
-
<option v-for="u in users" :key="u.id" :value="u.id">{{ u.name }} ({{ u.email }})</option>
|
|
211
|
-
</select>
|
|
212
|
-
</div>
|
|
213
|
-
</div>
|
|
214
|
-
|
|
215
|
-
<div>
|
|
216
|
-
<label
|
|
217
|
-
:class="dark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600'"
|
|
218
|
-
class="mb-1 block text-xs font-medium"
|
|
219
|
-
>Abilities</label
|
|
220
|
-
>
|
|
221
|
-
<div class="flex items-center gap-3">
|
|
222
|
-
<label
|
|
223
|
-
v-for="ability in ['agent', 'admin']"
|
|
224
|
-
:key="ability"
|
|
225
|
-
:class="dark ? 'text-[var(--esc-panel-text-secondary)]' : 'text-gray-700'"
|
|
226
|
-
class="flex items-center gap-1.5 text-sm cursor-pointer"
|
|
227
|
-
>
|
|
228
|
-
<input
|
|
229
|
-
type="checkbox"
|
|
230
|
-
:checked="form.abilities.includes(ability)"
|
|
231
|
-
class="rounded border-neutral-600"
|
|
232
|
-
@change="toggleAbility(ability)"
|
|
233
|
-
/>
|
|
234
|
-
{{ ability }}
|
|
235
|
-
</label>
|
|
236
|
-
</div>
|
|
237
|
-
</div>
|
|
238
|
-
|
|
239
|
-
<div>
|
|
240
|
-
<label
|
|
241
|
-
:class="dark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600'"
|
|
242
|
-
class="mb-1 block text-xs font-medium"
|
|
243
|
-
>Expires In (days)</label
|
|
244
|
-
>
|
|
245
|
-
<input
|
|
246
|
-
v-model.number="form.expires_in_days"
|
|
247
|
-
type="number"
|
|
248
|
-
min="1"
|
|
249
|
-
max="365"
|
|
250
|
-
placeholder="Leave empty for no expiration"
|
|
251
|
-
:class="
|
|
252
|
-
dark
|
|
253
|
-
? 'border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text)]'
|
|
254
|
-
: 'border-gray-300 bg-white text-gray-900'
|
|
255
|
-
"
|
|
256
|
-
class="w-48 rounded-lg border px-3 py-2 text-sm"
|
|
257
|
-
/>
|
|
258
|
-
</div>
|
|
259
|
-
|
|
260
|
-
<div class="flex items-center gap-3 pt-2">
|
|
261
|
-
<button
|
|
262
|
-
:disabled="form.processing || !form.name || !form.user_id"
|
|
263
|
-
:class="
|
|
264
|
-
dark
|
|
265
|
-
? 'bg-white text-black hover:bg-neutral-200 disabled:bg-[var(--esc-panel-text-muted)] disabled:text-[var(--esc-panel-text-muted)]'
|
|
266
|
-
: 'bg-gray-900 text-white hover:bg-gray-800 disabled:bg-gray-300 disabled:text-gray-500'
|
|
267
|
-
"
|
|
268
|
-
class="rounded-lg px-4 py-2 text-sm font-medium transition-colors"
|
|
269
|
-
@click="createToken"
|
|
270
|
-
>
|
|
271
|
-
{{ form.processing ? 'Creating...' : 'Create Token' }}
|
|
272
|
-
</button>
|
|
273
|
-
<button
|
|
274
|
-
:class="
|
|
275
|
-
dark
|
|
276
|
-
? 'text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text)]'
|
|
277
|
-
: 'text-gray-500 hover:text-gray-900'
|
|
278
|
-
"
|
|
279
|
-
class="text-sm transition-colors"
|
|
280
|
-
@click="showCreate = false"
|
|
281
|
-
>
|
|
282
|
-
Cancel
|
|
283
|
-
</button>
|
|
284
|
-
</div>
|
|
285
|
-
</div>
|
|
286
|
-
|
|
287
|
-
<!-- Tokens table -->
|
|
288
|
-
<div
|
|
289
|
-
:class="dark ? 'border-[var(--esc-panel-border)]' : 'border-gray-200'"
|
|
290
|
-
class="overflow-hidden rounded-lg border"
|
|
291
|
-
>
|
|
292
|
-
<table class="w-full text-sm">
|
|
293
|
-
<thead
|
|
294
|
-
:class="
|
|
295
|
-
dark
|
|
296
|
-
? 'bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-muted)]'
|
|
297
|
-
: 'bg-gray-50 text-gray-500'
|
|
298
|
-
"
|
|
299
|
-
>
|
|
300
|
-
<tr>
|
|
301
|
-
<th class="px-4 py-3 text-left text-xs font-medium uppercase">Name</th>
|
|
302
|
-
<th class="px-4 py-3 text-left text-xs font-medium uppercase">User</th>
|
|
303
|
-
<th class="px-4 py-3 text-left text-xs font-medium uppercase">Abilities</th>
|
|
304
|
-
<th class="px-4 py-3 text-left text-xs font-medium uppercase">Last Used</th>
|
|
305
|
-
<th class="px-4 py-3 text-left text-xs font-medium uppercase">Expires</th>
|
|
306
|
-
<th class="px-4 py-3 text-right text-xs font-medium uppercase">Actions</th>
|
|
307
|
-
</tr>
|
|
308
|
-
</thead>
|
|
309
|
-
<tbody :class="dark ? 'divide-[var(--esc-panel-border)]' : 'divide-gray-100'" class="divide-y">
|
|
310
|
-
<tr
|
|
311
|
-
v-for="token in tokens"
|
|
312
|
-
:key="token.id"
|
|
313
|
-
:class="[
|
|
314
|
-
dark ? 'hover:bg-[var(--esc-panel-hover)]' : 'hover:bg-gray-50',
|
|
315
|
-
token.is_expired ? 'opacity-50' : '',
|
|
316
|
-
]"
|
|
317
|
-
>
|
|
318
|
-
<td
|
|
319
|
-
:class="dark ? 'text-[var(--esc-panel-text)]' : 'text-gray-900'"
|
|
320
|
-
class="px-4 py-3 font-medium"
|
|
321
|
-
>
|
|
322
|
-
{{ token.name }}
|
|
323
|
-
</td>
|
|
324
|
-
<td
|
|
325
|
-
:class="dark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600'"
|
|
326
|
-
class="px-4 py-3"
|
|
327
|
-
>
|
|
328
|
-
{{ token.user_name }}
|
|
329
|
-
</td>
|
|
330
|
-
<td class="px-4 py-3">
|
|
331
|
-
<span
|
|
332
|
-
v-for="a in token.abilities || []"
|
|
333
|
-
:key="a"
|
|
334
|
-
:class="
|
|
335
|
-
dark
|
|
336
|
-
? 'bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-secondary)]'
|
|
337
|
-
: 'bg-gray-100 text-gray-700'
|
|
338
|
-
"
|
|
339
|
-
class="mr-1 rounded px-1.5 py-0.5 text-xs"
|
|
340
|
-
>{{ a }}</span
|
|
341
|
-
>
|
|
342
|
-
</td>
|
|
343
|
-
<td
|
|
344
|
-
:class="dark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500'"
|
|
345
|
-
class="px-4 py-3 text-xs"
|
|
346
|
-
>
|
|
347
|
-
<template v-if="token.last_used_at">{{ formatDate(token.last_used_at) }}</template>
|
|
348
|
-
<template v-else>Never</template>
|
|
349
|
-
</td>
|
|
350
|
-
<td
|
|
351
|
-
:class="dark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500'"
|
|
352
|
-
class="px-4 py-3 text-xs"
|
|
353
|
-
>
|
|
354
|
-
<span v-if="token.is_expired" class="text-rose-400">Expired</span>
|
|
355
|
-
<template v-else-if="token.expires_at">{{ formatDate(token.expires_at) }}</template>
|
|
356
|
-
<template v-else>Never</template>
|
|
357
|
-
</td>
|
|
358
|
-
<td class="px-4 py-3 text-right">
|
|
359
|
-
<button
|
|
360
|
-
class="text-xs text-rose-400 hover:text-rose-300 transition-colors"
|
|
361
|
-
@click="revokeToken(token.id)"
|
|
362
|
-
>
|
|
363
|
-
Revoke
|
|
364
|
-
</button>
|
|
365
|
-
</td>
|
|
366
|
-
</tr>
|
|
367
|
-
<tr v-if="!tokens.length">
|
|
368
|
-
<td
|
|
369
|
-
colspan="6"
|
|
370
|
-
:class="dark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-400'"
|
|
371
|
-
class="px-4 py-8 text-center text-sm"
|
|
372
|
-
>
|
|
373
|
-
No API tokens created yet.
|
|
374
|
-
</td>
|
|
375
|
-
</tr>
|
|
376
|
-
</tbody>
|
|
377
|
-
</table>
|
|
378
|
-
</div>
|
|
379
|
-
</div>
|
|
380
|
-
</EscalatedLayout>
|
|
381
|
-
</template>
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed, inject } from 'vue';
|
|
3
|
+
import { useForm, usePage } from '@inertiajs/vue3';
|
|
4
|
+
import EscalatedLayout from '../../../components/EscalatedLayout.vue';
|
|
5
|
+
|
|
6
|
+
defineProps({
|
|
7
|
+
tokens: Array,
|
|
8
|
+
users: Array,
|
|
9
|
+
api_enabled: Boolean,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const dark = inject(
|
|
13
|
+
'esc-dark',
|
|
14
|
+
computed(() => true),
|
|
15
|
+
);
|
|
16
|
+
const page = usePage();
|
|
17
|
+
const prefix = computed(() => {
|
|
18
|
+
const p = page.props.escalated?.prefix || 'support';
|
|
19
|
+
return p.startsWith('/') ? p : `/${p}`;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const showCreate = ref(false);
|
|
23
|
+
const copiedToken = ref(null);
|
|
24
|
+
|
|
25
|
+
const form = useForm({
|
|
26
|
+
name: '',
|
|
27
|
+
user_id: '',
|
|
28
|
+
abilities: ['agent'],
|
|
29
|
+
expires_in_days: null,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function createToken() {
|
|
33
|
+
form.post(`${prefix.value}/admin/api-tokens`, {
|
|
34
|
+
preserveScroll: true,
|
|
35
|
+
onSuccess: (page) => {
|
|
36
|
+
const flash = page.props.flash || {};
|
|
37
|
+
if (flash.plain_text_token) {
|
|
38
|
+
copiedToken.value = flash.plain_text_token;
|
|
39
|
+
}
|
|
40
|
+
form.reset();
|
|
41
|
+
showCreate.value = false;
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function revokeToken(id) {
|
|
47
|
+
if (!confirm('Are you sure you want to revoke this token? This cannot be undone.')) return;
|
|
48
|
+
useForm({}).delete(`${prefix.value}/admin/api-tokens/${id}`, { preserveScroll: true });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function copyToken() {
|
|
52
|
+
if (copiedToken.value) {
|
|
53
|
+
navigator.clipboard.writeText(copiedToken.value);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function dismissToken() {
|
|
58
|
+
copiedToken.value = null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function toggleAbility(ability) {
|
|
62
|
+
const idx = form.abilities.indexOf(ability);
|
|
63
|
+
if (idx >= 0) {
|
|
64
|
+
form.abilities.splice(idx, 1);
|
|
65
|
+
} else {
|
|
66
|
+
form.abilities.push(ability);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatDate(iso) {
|
|
71
|
+
if (!iso) return '—';
|
|
72
|
+
return new Date(iso).toLocaleDateString(undefined, {
|
|
73
|
+
year: 'numeric',
|
|
74
|
+
month: 'short',
|
|
75
|
+
day: 'numeric',
|
|
76
|
+
hour: '2-digit',
|
|
77
|
+
minute: '2-digit',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<template>
|
|
83
|
+
<EscalatedLayout title="API Tokens">
|
|
84
|
+
<div class="space-y-6">
|
|
85
|
+
<!-- Header -->
|
|
86
|
+
<div class="flex items-center justify-between">
|
|
87
|
+
<div>
|
|
88
|
+
<h2 :class="dark ? 'text-[var(--esc-panel-text)]' : 'text-gray-900'" class="text-lg font-semibold">
|
|
89
|
+
API Tokens
|
|
90
|
+
</h2>
|
|
91
|
+
<p :class="dark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500'" class="mt-1 text-sm">
|
|
92
|
+
Manage API tokens for desktop apps and external integrations.
|
|
93
|
+
</p>
|
|
94
|
+
</div>
|
|
95
|
+
<button
|
|
96
|
+
:class="
|
|
97
|
+
dark ? 'bg-white text-black hover:bg-neutral-200' : 'bg-gray-900 text-white hover:bg-gray-800'
|
|
98
|
+
"
|
|
99
|
+
class="rounded-lg px-4 py-2 text-sm font-medium transition-colors"
|
|
100
|
+
@click="showCreate = true"
|
|
101
|
+
>
|
|
102
|
+
Create Token
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<!-- Not enabled warning -->
|
|
107
|
+
<div
|
|
108
|
+
v-if="!api_enabled"
|
|
109
|
+
:class="
|
|
110
|
+
dark
|
|
111
|
+
? 'border-amber-500/30 bg-amber-500/10 text-amber-300'
|
|
112
|
+
: 'border-amber-300 bg-amber-50 text-amber-800'
|
|
113
|
+
"
|
|
114
|
+
class="rounded-lg border p-4 text-sm"
|
|
115
|
+
>
|
|
116
|
+
The REST API is currently disabled. Set <code class="font-mono">ESCALATED_API_ENABLED=true</code> in
|
|
117
|
+
your .env to enable it.
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<!-- Copied token modal -->
|
|
121
|
+
<div
|
|
122
|
+
v-if="copiedToken"
|
|
123
|
+
:class="dark ? 'border-emerald-500/30 bg-emerald-500/10' : 'border-emerald-300 bg-emerald-50'"
|
|
124
|
+
class="rounded-lg border p-4"
|
|
125
|
+
>
|
|
126
|
+
<p :class="dark ? 'text-emerald-300' : 'text-emerald-800'" class="text-sm font-medium">
|
|
127
|
+
Token created! Copy it now — it won't be shown again.
|
|
128
|
+
</p>
|
|
129
|
+
<div class="mt-2 flex items-center gap-2">
|
|
130
|
+
<code
|
|
131
|
+
:class="dark ? 'bg-black text-emerald-400' : 'bg-white text-emerald-700'"
|
|
132
|
+
class="flex-1 rounded border px-3 py-2 text-xs font-mono break-all"
|
|
133
|
+
:style="dark ? 'border-color: rgba(255,255,255,0.1)' : ''"
|
|
134
|
+
>{{ copiedToken }}</code
|
|
135
|
+
>
|
|
136
|
+
<button
|
|
137
|
+
:class="
|
|
138
|
+
dark
|
|
139
|
+
? 'bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text)] hover:bg-[var(--esc-panel-active)]'
|
|
140
|
+
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
|
141
|
+
"
|
|
142
|
+
class="rounded-lg px-3 py-2 text-sm font-medium transition-colors"
|
|
143
|
+
@click="copyToken"
|
|
144
|
+
>
|
|
145
|
+
Copy
|
|
146
|
+
</button>
|
|
147
|
+
<button
|
|
148
|
+
:class="
|
|
149
|
+
dark
|
|
150
|
+
? 'text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text)]'
|
|
151
|
+
: 'text-gray-400 hover:text-gray-600'
|
|
152
|
+
"
|
|
153
|
+
class="px-2 py-2 text-sm transition-colors"
|
|
154
|
+
@click="dismissToken"
|
|
155
|
+
>
|
|
156
|
+
Dismiss
|
|
157
|
+
</button>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<!-- Create form -->
|
|
162
|
+
<div
|
|
163
|
+
v-if="showCreate"
|
|
164
|
+
:class="
|
|
165
|
+
dark
|
|
166
|
+
? 'border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface-alt)]'
|
|
167
|
+
: 'border-gray-200 bg-white'
|
|
168
|
+
"
|
|
169
|
+
class="rounded-lg border p-5 space-y-4"
|
|
170
|
+
>
|
|
171
|
+
<h3 :class="dark ? 'text-[var(--esc-panel-text)]' : 'text-gray-900'" class="text-sm font-semibold">
|
|
172
|
+
New API Token
|
|
173
|
+
</h3>
|
|
174
|
+
|
|
175
|
+
<div class="grid grid-cols-2 gap-4">
|
|
176
|
+
<div>
|
|
177
|
+
<label
|
|
178
|
+
:class="dark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600'"
|
|
179
|
+
class="mb-1 block text-xs font-medium"
|
|
180
|
+
>Token Name</label
|
|
181
|
+
>
|
|
182
|
+
<input
|
|
183
|
+
v-model="form.name"
|
|
184
|
+
type="text"
|
|
185
|
+
placeholder="e.g. Desktop App - Work Laptop"
|
|
186
|
+
:class="
|
|
187
|
+
dark
|
|
188
|
+
? 'border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text)]'
|
|
189
|
+
: 'border-gray-300 bg-white text-gray-900'
|
|
190
|
+
"
|
|
191
|
+
class="w-full rounded-lg border px-3 py-2 text-sm"
|
|
192
|
+
/>
|
|
193
|
+
</div>
|
|
194
|
+
<div>
|
|
195
|
+
<label
|
|
196
|
+
:class="dark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600'"
|
|
197
|
+
class="mb-1 block text-xs font-medium"
|
|
198
|
+
>User</label
|
|
199
|
+
>
|
|
200
|
+
<select
|
|
201
|
+
v-model="form.user_id"
|
|
202
|
+
:class="
|
|
203
|
+
dark
|
|
204
|
+
? 'border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text)]'
|
|
205
|
+
: 'border-gray-300 bg-white text-gray-900'
|
|
206
|
+
"
|
|
207
|
+
class="w-full rounded-lg border px-3 py-2 text-sm"
|
|
208
|
+
>
|
|
209
|
+
<option value="">Select user...</option>
|
|
210
|
+
<option v-for="u in users" :key="u.id" :value="u.id">{{ u.name }} ({{ u.email }})</option>
|
|
211
|
+
</select>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<div>
|
|
216
|
+
<label
|
|
217
|
+
:class="dark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600'"
|
|
218
|
+
class="mb-1 block text-xs font-medium"
|
|
219
|
+
>Abilities</label
|
|
220
|
+
>
|
|
221
|
+
<div class="flex items-center gap-3">
|
|
222
|
+
<label
|
|
223
|
+
v-for="ability in ['agent', 'admin']"
|
|
224
|
+
:key="ability"
|
|
225
|
+
:class="dark ? 'text-[var(--esc-panel-text-secondary)]' : 'text-gray-700'"
|
|
226
|
+
class="flex items-center gap-1.5 text-sm cursor-pointer"
|
|
227
|
+
>
|
|
228
|
+
<input
|
|
229
|
+
type="checkbox"
|
|
230
|
+
:checked="form.abilities.includes(ability)"
|
|
231
|
+
class="rounded border-neutral-600"
|
|
232
|
+
@change="toggleAbility(ability)"
|
|
233
|
+
/>
|
|
234
|
+
{{ ability }}
|
|
235
|
+
</label>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<div>
|
|
240
|
+
<label
|
|
241
|
+
:class="dark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600'"
|
|
242
|
+
class="mb-1 block text-xs font-medium"
|
|
243
|
+
>Expires In (days)</label
|
|
244
|
+
>
|
|
245
|
+
<input
|
|
246
|
+
v-model.number="form.expires_in_days"
|
|
247
|
+
type="number"
|
|
248
|
+
min="1"
|
|
249
|
+
max="365"
|
|
250
|
+
placeholder="Leave empty for no expiration"
|
|
251
|
+
:class="
|
|
252
|
+
dark
|
|
253
|
+
? 'border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text)]'
|
|
254
|
+
: 'border-gray-300 bg-white text-gray-900'
|
|
255
|
+
"
|
|
256
|
+
class="w-48 rounded-lg border px-3 py-2 text-sm"
|
|
257
|
+
/>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<div class="flex items-center gap-3 pt-2">
|
|
261
|
+
<button
|
|
262
|
+
:disabled="form.processing || !form.name || !form.user_id"
|
|
263
|
+
:class="
|
|
264
|
+
dark
|
|
265
|
+
? 'bg-white text-black hover:bg-neutral-200 disabled:bg-[var(--esc-panel-text-muted)] disabled:text-[var(--esc-panel-text-muted)]'
|
|
266
|
+
: 'bg-gray-900 text-white hover:bg-gray-800 disabled:bg-gray-300 disabled:text-gray-500'
|
|
267
|
+
"
|
|
268
|
+
class="rounded-lg px-4 py-2 text-sm font-medium transition-colors"
|
|
269
|
+
@click="createToken"
|
|
270
|
+
>
|
|
271
|
+
{{ form.processing ? 'Creating...' : 'Create Token' }}
|
|
272
|
+
</button>
|
|
273
|
+
<button
|
|
274
|
+
:class="
|
|
275
|
+
dark
|
|
276
|
+
? 'text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text)]'
|
|
277
|
+
: 'text-gray-500 hover:text-gray-900'
|
|
278
|
+
"
|
|
279
|
+
class="text-sm transition-colors"
|
|
280
|
+
@click="showCreate = false"
|
|
281
|
+
>
|
|
282
|
+
Cancel
|
|
283
|
+
</button>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
<!-- Tokens table -->
|
|
288
|
+
<div
|
|
289
|
+
:class="dark ? 'border-[var(--esc-panel-border)]' : 'border-gray-200'"
|
|
290
|
+
class="overflow-hidden rounded-lg border"
|
|
291
|
+
>
|
|
292
|
+
<table class="w-full text-sm">
|
|
293
|
+
<thead
|
|
294
|
+
:class="
|
|
295
|
+
dark
|
|
296
|
+
? 'bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-muted)]'
|
|
297
|
+
: 'bg-gray-50 text-gray-500'
|
|
298
|
+
"
|
|
299
|
+
>
|
|
300
|
+
<tr>
|
|
301
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase">Name</th>
|
|
302
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase">User</th>
|
|
303
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase">Abilities</th>
|
|
304
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase">Last Used</th>
|
|
305
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase">Expires</th>
|
|
306
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase">Actions</th>
|
|
307
|
+
</tr>
|
|
308
|
+
</thead>
|
|
309
|
+
<tbody :class="dark ? 'divide-[var(--esc-panel-border)]' : 'divide-gray-100'" class="divide-y">
|
|
310
|
+
<tr
|
|
311
|
+
v-for="token in tokens"
|
|
312
|
+
:key="token.id"
|
|
313
|
+
:class="[
|
|
314
|
+
dark ? 'hover:bg-[var(--esc-panel-hover)]' : 'hover:bg-gray-50',
|
|
315
|
+
token.is_expired ? 'opacity-50' : '',
|
|
316
|
+
]"
|
|
317
|
+
>
|
|
318
|
+
<td
|
|
319
|
+
:class="dark ? 'text-[var(--esc-panel-text)]' : 'text-gray-900'"
|
|
320
|
+
class="px-4 py-3 font-medium"
|
|
321
|
+
>
|
|
322
|
+
{{ token.name }}
|
|
323
|
+
</td>
|
|
324
|
+
<td
|
|
325
|
+
:class="dark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600'"
|
|
326
|
+
class="px-4 py-3"
|
|
327
|
+
>
|
|
328
|
+
{{ token.user_name }}
|
|
329
|
+
</td>
|
|
330
|
+
<td class="px-4 py-3">
|
|
331
|
+
<span
|
|
332
|
+
v-for="a in token.abilities || []"
|
|
333
|
+
:key="a"
|
|
334
|
+
:class="
|
|
335
|
+
dark
|
|
336
|
+
? 'bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-secondary)]'
|
|
337
|
+
: 'bg-gray-100 text-gray-700'
|
|
338
|
+
"
|
|
339
|
+
class="mr-1 rounded px-1.5 py-0.5 text-xs"
|
|
340
|
+
>{{ a }}</span
|
|
341
|
+
>
|
|
342
|
+
</td>
|
|
343
|
+
<td
|
|
344
|
+
:class="dark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500'"
|
|
345
|
+
class="px-4 py-3 text-xs"
|
|
346
|
+
>
|
|
347
|
+
<template v-if="token.last_used_at">{{ formatDate(token.last_used_at) }}</template>
|
|
348
|
+
<template v-else>Never</template>
|
|
349
|
+
</td>
|
|
350
|
+
<td
|
|
351
|
+
:class="dark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500'"
|
|
352
|
+
class="px-4 py-3 text-xs"
|
|
353
|
+
>
|
|
354
|
+
<span v-if="token.is_expired" class="text-rose-400">Expired</span>
|
|
355
|
+
<template v-else-if="token.expires_at">{{ formatDate(token.expires_at) }}</template>
|
|
356
|
+
<template v-else>Never</template>
|
|
357
|
+
</td>
|
|
358
|
+
<td class="px-4 py-3 text-right">
|
|
359
|
+
<button
|
|
360
|
+
class="text-xs text-rose-400 hover:text-rose-300 transition-colors"
|
|
361
|
+
@click="revokeToken(token.id)"
|
|
362
|
+
>
|
|
363
|
+
Revoke
|
|
364
|
+
</button>
|
|
365
|
+
</td>
|
|
366
|
+
</tr>
|
|
367
|
+
<tr v-if="!tokens.length">
|
|
368
|
+
<td
|
|
369
|
+
colspan="6"
|
|
370
|
+
:class="dark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-400'"
|
|
371
|
+
class="px-4 py-8 text-center text-sm"
|
|
372
|
+
>
|
|
373
|
+
No API tokens created yet.
|
|
374
|
+
</td>
|
|
375
|
+
</tr>
|
|
376
|
+
</tbody>
|
|
377
|
+
</table>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
</EscalatedLayout>
|
|
381
|
+
</template>
|