Point from the dashboard to Stalwart's own administration
A line under the cards says where the rest is: detailed metrics, the
delivery queue, logs and server settings are in Stalwart's own
administration. It links there when the operator sets STALWART_ADMIN_URL,
and stays plain text otherwise, because STALWART_URL is how this server
reaches Stalwart and is often an address no browser can open.
Several servers: a servers file entry may now be an object,
{"url": ..., "adminUrl": ...}, and a session routed to that server gets its
adminUrl. A routed domain without one gets no link rather than the default
server's, for the same reason routing never falls back. The URL is sent
only to a session that may administer.
The shipped example file stopped the server at startup: its "_comment"
key was read as a domain and refused as not a URL, while the test that
checks the example skipped it. Keys starting with an underscore are notes
now -- no mail domain starts with one -- and the example is also loaded
through the real parser in a test, so the two cannot disagree again.
Two new strings, in all nine catalogues.
This commit is contained in:
@@ -38,6 +38,8 @@ export interface JmapSession {
|
||||
server?: {
|
||||
/** "oss" | "community" | "enterprise". Stalwart publishes no version. */
|
||||
edition?: string | null;
|
||||
/** Where Stalwart's own administration is (STALWART_ADMIN_URL), for a session that may administer. */
|
||||
adminUrl?: string | null;
|
||||
};
|
||||
/**
|
||||
* False when this session may not administer: the operator turned it off,
|
||||
|
||||
@@ -147,6 +147,8 @@ export const catalog: Catalog = {
|
||||
"The numbers your role can see, as the server reports them.": "Die Zahlen, die Ihre Rolle sehen darf, so wie der Server sie meldet.",
|
||||
"Nothing to show": "Nichts anzuzeigen",
|
||||
"Could not be loaded": "Konnte nicht geladen werden",
|
||||
"Detailed metrics, the delivery queue, logs and server settings are in Stalwart's own administration.": "Detaillierte Metriken, die Zustellwarteschlange, Protokolle und Servereinstellungen finden Sie in der Verwaltung von Stalwart selbst.",
|
||||
"Open Stalwart admin": "Stalwart-Verwaltung öffnen",
|
||||
"User": "Benutzer",
|
||||
"Administrator": "Administrator",
|
||||
"Custom role": "Eigene Rolle",
|
||||
|
||||
@@ -139,6 +139,8 @@ export const catalog: Catalog = {
|
||||
"The numbers your role can see, as the server reports them.": "Las cifras que su rol puede ver, tal como las informa el servidor.",
|
||||
"Nothing to show": "Nada que mostrar",
|
||||
"Could not be loaded": "No se pudo cargar",
|
||||
"Detailed metrics, the delivery queue, logs and server settings are in Stalwart's own administration.": "Las métricas detalladas, la cola de entrega, los registros y la configuración del servidor están en la administración de Stalwart.",
|
||||
"Open Stalwart admin": "Abrir la administración de Stalwart",
|
||||
"User": "Usuario",
|
||||
"Administrator": "Administrador",
|
||||
"Custom role": "Rol personalizado",
|
||||
|
||||
@@ -144,6 +144,8 @@ export const catalog: Catalog = {
|
||||
"The numbers your role can see, as the server reports them.": "Les chiffres que votre rôle permet de voir, tels que le serveur les indique.",
|
||||
"Nothing to show": "Rien à afficher",
|
||||
"Could not be loaded": "Chargement impossible",
|
||||
"Detailed metrics, the delivery queue, logs and server settings are in Stalwart's own administration.": "Les métriques détaillées, la file de distribution, les journaux et les réglages du serveur se trouvent dans l’administration de Stalwart.",
|
||||
"Open Stalwart admin": "Ouvrir l’administration de Stalwart",
|
||||
"User": "Utilisateur",
|
||||
"Administrator": "Administrateur",
|
||||
"Custom role": "Rôle personnalisé",
|
||||
|
||||
@@ -138,6 +138,8 @@ export const catalog: Catalog = {
|
||||
"The numbers your role can see, as the server reports them.": "あなたのロールで見られる数値を、サーバーの報告どおりに表示します。",
|
||||
"Nothing to show": "表示するものはありません",
|
||||
"Could not be loaded": "読み込めませんでした",
|
||||
"Detailed metrics, the delivery queue, logs and server settings are in Stalwart's own administration.": "詳しいメトリクス、配送キュー、ログ、サーバー設定は Stalwart 自体の管理画面にあります。",
|
||||
"Open Stalwart admin": "Stalwart の管理画面を開く",
|
||||
"User": "ユーザー",
|
||||
"Administrator": "管理者",
|
||||
"Custom role": "カスタムロール",
|
||||
|
||||
@@ -135,6 +135,8 @@ export const catalog: Catalog = {
|
||||
"The numbers your role can see, as the server reports them.": "De cijfers die uw rol mag zien, zoals de server ze meldt.",
|
||||
"Nothing to show": "Niets om te tonen",
|
||||
"Could not be loaded": "Kon niet worden geladen",
|
||||
"Detailed metrics, the delivery queue, logs and server settings are in Stalwart's own administration.": "Gedetailleerde statistieken, de bezorgwachtrij, logboeken en serverinstellingen vindt u in het eigen beheer van Stalwart.",
|
||||
"Open Stalwart admin": "Stalwart-beheer openen",
|
||||
"User": "Gebruiker",
|
||||
"Administrator": "Beheerder",
|
||||
"Custom role": "Aangepaste rol",
|
||||
|
||||
@@ -142,6 +142,8 @@ export const catalog: Catalog = {
|
||||
"The numbers your role can see, as the server reports them.": "Os números que sua função pode ver, como o servidor os informa.",
|
||||
"Nothing to show": "Nada para mostrar",
|
||||
"Could not be loaded": "Não foi possível carregar",
|
||||
"Detailed metrics, the delivery queue, logs and server settings are in Stalwart's own administration.": "Métricas detalhadas, a fila de entrega, os logs e as configurações do servidor ficam na administração do próprio Stalwart.",
|
||||
"Open Stalwart admin": "Abrir a administração do Stalwart",
|
||||
"User": "Usuário",
|
||||
"Administrator": "Administrador",
|
||||
"Custom role": "Função personalizada",
|
||||
|
||||
@@ -141,6 +141,8 @@ export const catalog: Catalog = {
|
||||
"The numbers your role can see, as the server reports them.": "Показатели, доступные вашей роли, в том виде, в каком их сообщает сервер.",
|
||||
"Nothing to show": "Нечего показать",
|
||||
"Could not be loaded": "Не удалось загрузить",
|
||||
"Detailed metrics, the delivery queue, logs and server settings are in Stalwart's own administration.": "Подробные метрики, очередь доставки, журналы и настройки сервера находятся в собственной панели администрирования Stalwart.",
|
||||
"Open Stalwart admin": "Открыть администрирование Stalwart",
|
||||
"User": "Пользователь",
|
||||
"Administrator": "Администратор",
|
||||
"Custom role": "Особая роль",
|
||||
|
||||
@@ -135,6 +135,8 @@ export const catalog: Catalog = {
|
||||
"The numbers your role can see, as the server reports them.": "Показники, доступні вашій ролі, у тому вигляді, як їх повідомляє сервер.",
|
||||
"Nothing to show": "Нічого показати",
|
||||
"Could not be loaded": "Не вдалося завантажити",
|
||||
"Detailed metrics, the delivery queue, logs and server settings are in Stalwart's own administration.": "Докладні метрики, черга доставки, журнали та налаштування сервера є у власній панелі адміністрування Stalwart.",
|
||||
"Open Stalwart admin": "Відкрити адміністрування Stalwart",
|
||||
"User": "Користувач",
|
||||
"Administrator": "Адміністратор",
|
||||
"Custom role": "Власна роль",
|
||||
|
||||
@@ -137,6 +137,8 @@ export const catalog: Catalog = {
|
||||
"The numbers your role can see, as the server reports them.": "您的角色可以查看的数字,按服务器报告显示。",
|
||||
"Nothing to show": "没有可显示的内容",
|
||||
"Could not be loaded": "无法加载",
|
||||
"Detailed metrics, the delivery queue, logs and server settings are in Stalwart's own administration.": "详细指标、投递队列、日志和服务器设置位于 Stalwart 自身的管理界面中。",
|
||||
"Open Stalwart admin": "打开 Stalwart 管理界面",
|
||||
"User": "用户",
|
||||
"Administrator": "管理员",
|
||||
"Custom role": "自定义角色",
|
||||
|
||||
@@ -1750,6 +1750,9 @@ select optgroup { background-color: var(--bg-elev); color: var(--fg); }
|
||||
pane beside it can be any width. */
|
||||
.admin-dashboard { max-width: calc(var(--cols-wide, 3) * 300px + (var(--cols-wide, 3) - 1) * 12px); }
|
||||
.admin-cards-wrap { container: admin-cards / inline-size; margin-top: 8px; }
|
||||
.admin-dashboard-note { margin-top: 16px; }
|
||||
.admin-dashboard-note a { white-space: nowrap; }
|
||||
.admin-dashboard-note svg { vertical-align: -1px; }
|
||||
.admin-cards { display: grid; gap: 12px; grid-template-columns: repeat(var(--cols-wide, 3), minmax(0, 1fr)); max-width: calc(var(--cols-wide, 3) * 300px + (var(--cols-wide, 3) - 1) * 12px); }
|
||||
@container admin-cards (max-width: 760px) { .admin-cards { grid-template-columns: repeat(var(--cols-mid, 2), minmax(0, 1fr)); max-width: calc(var(--cols-mid, 2) * 300px + (var(--cols-mid, 2) - 1) * 12px); } }
|
||||
@container admin-cards (max-width: 480px) { .admin-cards { grid-template-columns: minmax(0, 1fr); max-width: none; } }
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import { Link } from "wouter";
|
||||
import { ArrowDownToLine, ArrowUpFromLine, Globe, Hourglass, LayoutDashboard, MemoryStick, RefreshCw, Users } from "lucide-react";
|
||||
import { ArrowDownToLine, ArrowUpFromLine, ExternalLink, Globe, Hourglass, LayoutDashboard, MemoryStick, RefreshCw, Users } from "lucide-react";
|
||||
import { adminSections, dashboardCards, type DashboardCard } from "@/lib/adminAccess";
|
||||
import { balancedColumns, countObjects, DASHBOARD_WINDOW_MS, isRefused, loadMetrics, summariseMetrics, type MessageStats } from "@/lib/adminDashboard";
|
||||
import { formatDayMonthTime, resolvedLocale } from "@/lib/datetime";
|
||||
import { formatSize } from "@/lib/format";
|
||||
import { t } from "@/lib/i18n";
|
||||
import { Empty } from "@/ui/misc";
|
||||
import { useSession } from "@/store/session";
|
||||
import { usePermissions } from "./usePermissions";
|
||||
|
||||
/** Loading, a number, refused by the server (the card goes), or failed (the card says so). */
|
||||
@@ -33,6 +34,7 @@ async function settle<T>(work: Promise<T>): Promise<Loaded<T>> {
|
||||
*/
|
||||
export function AdminDashboard() {
|
||||
const perms = usePermissions();
|
||||
const adminUrl = useSession((s) => s.session?.ihasmail?.server?.adminUrl ?? null);
|
||||
const cards = dashboardCards(perms);
|
||||
const sections = adminSections(perms);
|
||||
const [reload, setReload] = useState(0);
|
||||
@@ -109,6 +111,20 @@ export function AdminDashboard() {
|
||||
) : (
|
||||
<Empty icon={<LayoutDashboard size={32} />} title={t("Nothing to show")} />
|
||||
)}
|
||||
{/* The line between the two interfaces, said where someone looking for
|
||||
more numbers will be: this is a glance, and operating the server is
|
||||
Stalwart's own administration. The link is the operator's to give. */}
|
||||
<p className="hint admin-dashboard-note">
|
||||
{t("Detailed metrics, the delivery queue, logs and server settings are in Stalwart's own administration.")}
|
||||
{adminUrl && (
|
||||
<>
|
||||
{" "}
|
||||
<a href={adminUrl} target="_blank" rel="noopener noreferrer">
|
||||
{t("Open Stalwart admin")} <ExternalLink size={13} aria-hidden="true" />
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -111,3 +111,40 @@ describe("the Administration dashboard", () => {
|
||||
expect(cards()[4]).toEqual(["Received", "—", "Could not be loaded"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the pointer to Stalwart's own administration", () => {
|
||||
let host: HTMLDivElement;
|
||||
let root: Root;
|
||||
beforeEach(() => {
|
||||
host = document.createElement("div");
|
||||
document.body.appendChild(host);
|
||||
root = createRoot(host);
|
||||
});
|
||||
afterEach(async () => {
|
||||
await act(async () => root.unmount());
|
||||
host.remove();
|
||||
});
|
||||
const renderWith = async (adminUrl: string | null) => {
|
||||
useSession.setState({ session: { capabilities: {}, accounts: {}, primaryAccounts: {}, username: "[email protected]", ihasmail: { permissions: HELPDESK, server: { edition: "enterprise", adminUrl } } } as unknown as JmapSession });
|
||||
const { hook } = memoryLocation({ path: "/admin" });
|
||||
await act(async () => {
|
||||
root.render(<Router hook={hook}><AdminDashboard /></Router>);
|
||||
});
|
||||
await act(async () => {});
|
||||
};
|
||||
|
||||
it("names it, and links it where the operator has said where it is", async () => {
|
||||
await renderWith("https://admin.example.com");
|
||||
const note = host.querySelector(".admin-dashboard-note")!;
|
||||
expect(note.textContent).toContain("Stalwart's own administration");
|
||||
const link = note.querySelector("a")!;
|
||||
expect(link.getAttribute("href")).toBe("https://admin.example.com");
|
||||
expect(link.getAttribute("rel")).toBe("noopener noreferrer");
|
||||
});
|
||||
|
||||
it("names it without a link where nobody has", async () => {
|
||||
await renderWith(null);
|
||||
expect(host.querySelector(".admin-dashboard-note")?.textContent).toContain("Stalwart's own administration");
|
||||
expect(host.querySelector(".admin-dashboard-note a")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user