apps/ui: Warp's design system for Vue 3 + Tailwind v4. Tokens, fonts, icons and logo come from the brand package, with a Tailwind theme on the tokens and Vue components on the brand component styles. apps/admin: the admin panel as a separate, optional service. warp-admin embeds the built Vue app and proxies only the login endpoints and /_warp/admin/v1 to the homeserver. Pages: dashboard, users, rooms, federation, registration and tokens, server notices, application services, audit log. Also: pnpm workspace, go.work entry, Dockerfile, an `admin` compose profile, Makefile targets, a CI job with a proxy smoke test, and docs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
118 lines
4.6 KiB
Vue
118 lines
4.6 KiB
Vue
<script setup lang="ts">
|
|
import { onMounted, ref } from 'vue'
|
|
import { WBadge, WBanner, WButton } from '@warp/ui'
|
|
import { listAppServices } from '@/api/admin'
|
|
import type { Namespace } from '@/api/types'
|
|
import LoadState from '@/components/LoadState.vue'
|
|
import Mxid from '@/components/Mxid.vue'
|
|
import PageHeader from '@/components/PageHeader.vue'
|
|
import { formatNumber, formatRelative } from '@/lib/format'
|
|
import { useLoad } from '@/lib/useLoad'
|
|
|
|
const services = useLoad(listAppServices)
|
|
const now = ref(Date.now())
|
|
onMounted(services.load)
|
|
|
|
function reload() {
|
|
now.value = Date.now()
|
|
services.load()
|
|
}
|
|
|
|
const kinds: { key: 'users' | 'aliases' | 'rooms'; label: string }[] = [
|
|
{ key: 'users', label: 'Users' },
|
|
{ key: 'aliases', label: 'Aliases' },
|
|
{ key: 'rooms', label: 'Rooms' },
|
|
]
|
|
|
|
function list(ns: Namespace[] | undefined) {
|
|
return ns ?? []
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<PageHeader
|
|
title="Application services"
|
|
description="Bridges and bots registered with this homeserver. Registrations are files read at startup, so this is a view, not an editor."
|
|
>
|
|
<template #actions>
|
|
<WButton icon="refresh" :loading="services.loading.value" @click="reload">Refresh</WButton>
|
|
</template>
|
|
</PageHeader>
|
|
|
|
<LoadState
|
|
:loading="services.loading.value"
|
|
:error="services.error.value"
|
|
:has-data="!!services.data.value"
|
|
@retry="services.load"
|
|
/>
|
|
|
|
<template v-if="services.data.value">
|
|
<p v-if="services.data.value.appservices.length === 0" class="wp-card p-8 text-center text-ink-muted">
|
|
No application services. Add registration files under <code class="code">app_service_api.config_files</code> and
|
|
restart.
|
|
</p>
|
|
|
|
<div class="flex flex-col gap-4">
|
|
<section v-for="s in services.data.value.appservices" :key="s.id" class="wp-card flex flex-col gap-4 p-4">
|
|
<div class="flex flex-wrap items-start gap-3">
|
|
<div class="min-w-0 flex-1">
|
|
<h2 class="subheading m-0">{{ s.id }}</h2>
|
|
<Mxid :value="s.sender" class="text-ink-muted" />
|
|
</div>
|
|
<div class="flex flex-wrap gap-1">
|
|
<WBadge v-if="s.rate_limited" tone="neutral">Rate limited</WBadge>
|
|
<template v-if="s.delivery">
|
|
<WBadge v-if="s.delivery.failures === 0" tone="success" icon="shield-check">Delivering</WBadge>
|
|
<WBadge v-else tone="warning" icon="shield-alert">{{ s.delivery.failures }} failed in a row</WBadge>
|
|
</template>
|
|
<WBadge v-else tone="neutral">No URL, sent nothing</WBadge>
|
|
</div>
|
|
</div>
|
|
|
|
<WBanner v-if="s.delivery && s.delivery.failures > 0" tone="warning">
|
|
{{ s.delivery.last_error || 'Delivery is failing.' }}
|
|
<template v-if="s.delivery.retry_at_ms">Next try {{ formatRelative(s.delivery.retry_at_ms, now) }}.</template>
|
|
</WBanner>
|
|
|
|
<dl class="grid grid-cols-2 gap-x-6 gap-y-3 sm:grid-cols-4">
|
|
<div v-if="s.url" class="col-span-2">
|
|
<dt class="caption text-ink-muted">URL</dt>
|
|
<dd class="m-0"><Mxid :value="s.url" /></dd>
|
|
</div>
|
|
<template v-if="s.delivery">
|
|
<div>
|
|
<dt class="caption text-ink-muted">Events delivered</dt>
|
|
<dd class="m-0 tabular-nums">{{ formatNumber(s.delivery.events_delivered) }}</dd>
|
|
</div>
|
|
<div>
|
|
<dt class="caption text-ink-muted">Behind</dt>
|
|
<dd class="m-0 tabular-nums">{{ formatNumber(s.delivery.behind) }} positions</dd>
|
|
</div>
|
|
<div>
|
|
<dt class="caption text-ink-muted">Last success</dt>
|
|
<dd class="m-0">{{ formatRelative(s.delivery.last_success_ms, now) }}</dd>
|
|
</div>
|
|
<div>
|
|
<dt class="caption text-ink-muted">Transaction</dt>
|
|
<dd class="m-0 tabular-nums">{{ formatNumber(s.delivery.txn_id) }}</dd>
|
|
</div>
|
|
</template>
|
|
</dl>
|
|
|
|
<div class="grid gap-4 border-t border-line pt-4 md:grid-cols-3">
|
|
<div v-for="k in kinds" :key="k.key">
|
|
<p class="caption mb-1 text-ink-muted">{{ k.label }} namespace</p>
|
|
<ul v-if="list(s.namespaces[k.key]).length" class="m-0 flex list-none flex-col gap-1 p-0">
|
|
<li v-for="n in list(s.namespaces[k.key])" :key="n.regex" class="flex items-center gap-2">
|
|
<code class="code truncate" :title="n.regex">{{ n.regex }}</code>
|
|
<WBadge v-if="n.exclusive" tone="info">Exclusive</WBadge>
|
|
</li>
|
|
</ul>
|
|
<p v-else class="small text-ink-muted">None</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</template>
|
|
</template>
|