Use American English spelling throughout
This commit is contained in:
@@ -270,7 +270,7 @@ export function AccountSheet({ account, ctx, onClose, onChanged, onCreated, onDe
|
||||
<p className="hint">
|
||||
{self
|
||||
? t("You can't move your own account into a tenant.")
|
||||
: t("An account can be in the tenant its domain is in. In a tenant it is limited by the tenant's role and counts towards its limits, and Administrator means administrator of that tenant.")}
|
||||
: t("An account can be in the tenant its domain is in. In a tenant it is limited by the tenant's role and counts toward its limits, and Administrator means administrator of that tenant.")}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -51,22 +51,22 @@ export function AccountsAdmin({ selectedId }: { selectedId?: string }) {
|
||||
}, [text]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let canceled = false;
|
||||
setError(null);
|
||||
void (async () => {
|
||||
try {
|
||||
const q = await queryAccounts({ type: "User", text: query, position, limit: PAGE_SIZE });
|
||||
const accounts = await getAccounts(q.ids);
|
||||
if (!cancelled) setPage({ accounts, total: q.total });
|
||||
if (!canceled) setPage({ accounts, total: q.total });
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
if (!canceled) {
|
||||
setPage({ accounts: [], total: 0 });
|
||||
setError(describeDirectoryError(err));
|
||||
}
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
canceled = true;
|
||||
};
|
||||
}, [query, position, reload]);
|
||||
|
||||
@@ -85,13 +85,13 @@ export function AccountsAdmin({ selectedId }: { selectedId?: string }) {
|
||||
setLoose(null);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
let canceled = false;
|
||||
void getAccounts([selectedId]).then(
|
||||
([a]) => { if (!cancelled) setLoose(a ?? null); },
|
||||
() => { if (!cancelled) setLoose(null); },
|
||||
([a]) => { if (!canceled) setLoose(a ?? null); },
|
||||
() => { if (!canceled) setLoose(null); },
|
||||
);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
canceled = true;
|
||||
};
|
||||
}, [selectedId, page]);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useState, type ReactNode } from "react";
|
||||
import { Link } from "wouter";
|
||||
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 { balancedColumns, countObjects, DASHBOARD_WINDOW_MS, isRefused, loadMetrics, summarizeMetrics, type MessageStats } from "@/lib/adminDashboard";
|
||||
import { formatDayMonthTime, resolvedLocale } from "@/lib/datetime";
|
||||
import { formatSize } from "@/lib/format";
|
||||
import { t } from "@/lib/i18n";
|
||||
@@ -45,17 +45,17 @@ export function AdminDashboard() {
|
||||
const key = cards.join(",");
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let canceled = false;
|
||||
const into = <T,>(set: (v: Loaded<T>) => void, work: () => Promise<T>) => {
|
||||
set(LOADING);
|
||||
void settle(work()).then((v) => !cancelled && set(v));
|
||||
void settle(work()).then((v) => !canceled && set(v));
|
||||
};
|
||||
if (cards.includes("users")) into(setUsers, () => countObjects("Account"));
|
||||
if (cards.includes("domains")) into(setDomains, () => countObjects("Domain"));
|
||||
if (cards.includes("pending")) into(setPending, () => countObjects("QueuedMessage"));
|
||||
if (cards.includes("received")) into(setMessages, async () => summariseMetrics(await loadMetrics(new Date(Date.now() - DASHBOARD_WINDOW_MS))));
|
||||
if (cards.includes("received")) into(setMessages, async () => summarizeMetrics(await loadMetrics(new Date(Date.now() - DASHBOARD_WINDOW_MS))));
|
||||
return () => {
|
||||
cancelled = true;
|
||||
canceled = true;
|
||||
};
|
||||
// `key` is the card list's contents; the array itself is new every render.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
listDkimKeys,
|
||||
looksLikeDomain,
|
||||
namesOf,
|
||||
normaliseDomain,
|
||||
normalizeDomain,
|
||||
parseZoneFile,
|
||||
updateDomain,
|
||||
type DirectoryDomainFull,
|
||||
@@ -76,11 +76,11 @@ export function DomainSheet({ id, accountCount, onClose, onChanged, onCreated, o
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
let cancelled = false;
|
||||
let canceled = false;
|
||||
void (async () => {
|
||||
try {
|
||||
const [d] = await getDomains([id], { zoneFile: true });
|
||||
if (cancelled) return;
|
||||
if (canceled) return;
|
||||
if (!d) {
|
||||
setLoadError(t("This domain no longer exists. Someone may have removed it."));
|
||||
return;
|
||||
@@ -91,18 +91,18 @@ export function DomainSheet({ id, accountCount, onClose, onChanged, onCreated, o
|
||||
setCatchAll(d.catchAllAddress ?? "");
|
||||
setPlus(d.subAddressing?.["@type"] ?? "Enabled");
|
||||
if (can(perms, "DkimSignature", "Query") && can(perms, "DkimSignature", "Get")) {
|
||||
void listDkimKeys(id).then((k) => { if (!cancelled) setKeys(k); }, () => { if (!cancelled) setKeys(null); });
|
||||
void listDkimKeys(id).then((k) => { if (!canceled) setKeys(k); }, () => { if (!canceled) setKeys(null); });
|
||||
}
|
||||
const serverId = d.dnsManagement?.["@type"] === "Automatic" ? d.dnsManagement.dnsServerId : undefined;
|
||||
if (serverId && can(perms, "DnsServer", "Get")) {
|
||||
void namesOf("DnsServer", [serverId]).then((n) => { if (!cancelled) setProvider(n.get(serverId) ?? null); }, () => {});
|
||||
void namesOf("DnsServer", [serverId]).then((n) => { if (!canceled) setProvider(n.get(serverId) ?? null); }, () => {});
|
||||
}
|
||||
} catch (err) {
|
||||
if (!cancelled) setLoadError(describeDirectoryError(err, "domain"));
|
||||
if (!canceled) setLoadError(describeDirectoryError(err, "domain"));
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
canceled = true;
|
||||
};
|
||||
}, [id, perms, revision]);
|
||||
|
||||
@@ -126,14 +126,14 @@ export function DomainSheet({ id, accountCount, onClose, onChanged, onCreated, o
|
||||
return;
|
||||
}
|
||||
const newId = await createDomain({ name, description });
|
||||
toast.success(t("Added {name}. Its DNS records are ready to copy.", { name: normaliseDomain(name) }));
|
||||
toast.success(t("Added {name}. Its DNS records are ready to copy.", { name: normalizeDomain(name) }));
|
||||
onCreated(newId);
|
||||
return;
|
||||
}
|
||||
if (!domain) return;
|
||||
const patch: Record<string, unknown> = {};
|
||||
if ((domain.description ?? "") !== description) patch.description = description.trim() || null;
|
||||
const nextAliases = [...new Set(aliases.map(normaliseDomain).filter(Boolean))];
|
||||
const nextAliases = [...new Set(aliases.map(normalizeDomain).filter(Boolean))];
|
||||
if (JSON.stringify(Object.keys(domain.aliases ?? {}).sort()) !== JSON.stringify([...nextAliases].sort())) {
|
||||
patch.aliases = Object.fromEntries(nextAliases.map((a) => [a, true]));
|
||||
}
|
||||
@@ -304,7 +304,7 @@ export function DomainSheet({ id, accountCount, onClose, onChanged, onCreated, o
|
||||
function AliasList({ aliases, setAliases, editable }: { aliases: string[]; setAliases: (a: string[]) => void; editable: boolean }) {
|
||||
const [value, setValue] = useState("");
|
||||
const add = () => {
|
||||
const name = normaliseDomain(value);
|
||||
const name = normalizeDomain(value);
|
||||
if (!looksLikeDomain(name) || aliases.includes(name)) return;
|
||||
setAliases([...aliases, name]);
|
||||
setValue("");
|
||||
@@ -376,7 +376,7 @@ function RemoveDomain({ domain, accountCount, keys, canRemoveKeys, onDeleted }:
|
||||
<button className="btn" onClick={() => setOpen(false)}>{t("Cancel")}</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
disabled={busy || normaliseDomain(typed) !== domain.name}
|
||||
disabled={busy || normalizeDomain(typed) !== domain.name}
|
||||
onClick={async () => {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
|
||||
@@ -32,27 +32,27 @@ export function DomainsAdmin({ selectedId }: { selectedId?: string }) {
|
||||
}, [text]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let canceled = false;
|
||||
setError(null);
|
||||
void (async () => {
|
||||
try {
|
||||
const q = await queryDomains({ text: query, position, limit: PAGE_SIZE });
|
||||
const domains = await getDomains(q.ids);
|
||||
if (cancelled) return;
|
||||
if (canceled) return;
|
||||
setPage({ domains, total: q.total });
|
||||
// Both are extras on top of the list, and each needs a permission of its own.
|
||||
if (can(perms, "Account", "Query")) void countAccounts(domains.map((d) => d.id)).then((c) => { if (!cancelled) setCounts(c); });
|
||||
if (can(perms, "Account", "Query")) void countAccounts(domains.map((d) => d.id)).then((c) => { if (!canceled) setCounts(c); });
|
||||
const tenantIds = [...new Set(domains.map((d) => d.memberTenantId).filter((x): x is string => Boolean(x)))];
|
||||
if (tenantIds.length && can(perms, "Tenant", "Get")) void namesOf("Tenant", tenantIds).then((n) => { if (!cancelled) setTenants(n); }, () => {});
|
||||
if (tenantIds.length && can(perms, "Tenant", "Get")) void namesOf("Tenant", tenantIds).then((n) => { if (!canceled) setTenants(n); }, () => {});
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
if (!canceled) {
|
||||
setPage({ domains: [], total: 0 });
|
||||
setError(describeDirectoryError(err, "domain"));
|
||||
}
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
canceled = true;
|
||||
};
|
||||
}, [query, position, reload, perms]);
|
||||
|
||||
|
||||
@@ -78,18 +78,18 @@ export function GroupSheet({ group, ctx, onClose, onChanged, onCreated, onDelete
|
||||
|
||||
useEffect(() => {
|
||||
if (!group) return;
|
||||
let cancelled = false;
|
||||
let canceled = false;
|
||||
setMembersError(null);
|
||||
listMembers(group.id).then(
|
||||
(m) => !cancelled && setMembers(m),
|
||||
(m) => !canceled && setMembers(m),
|
||||
(err) => {
|
||||
if (cancelled) return;
|
||||
if (canceled) return;
|
||||
setMembers({ members: [], total: 0 });
|
||||
setMembersError(describeDirectoryError(err, "group"));
|
||||
},
|
||||
);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
canceled = true;
|
||||
};
|
||||
}, [group, membersRevision]);
|
||||
|
||||
@@ -348,15 +348,15 @@ function AddMember({ ctx, exclude, busy, onAdd }: { ctx: DirectoryContext; exclu
|
||||
setFound(null);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
let canceled = false;
|
||||
const id = window.setTimeout(() => {
|
||||
searchUsers(needle).then(
|
||||
(list) => !cancelled && setFound(list),
|
||||
() => !cancelled && setFound([]),
|
||||
(list) => !canceled && setFound(list),
|
||||
() => !canceled && setFound([]),
|
||||
);
|
||||
}, 250);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
canceled = true;
|
||||
window.clearTimeout(id);
|
||||
};
|
||||
}, [text]);
|
||||
|
||||
@@ -45,24 +45,24 @@ export function GroupsAdmin({ selectedId }: { selectedId?: string }) {
|
||||
}, [text]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let canceled = false;
|
||||
setError(null);
|
||||
void (async () => {
|
||||
try {
|
||||
const q = await queryGroups({ text: query, position, limit: PAGE_SIZE });
|
||||
const groups = await getGroups(q.ids);
|
||||
if (cancelled) return;
|
||||
if (canceled) return;
|
||||
setPage({ groups, total: q.total });
|
||||
void countMembers(groups.map((g) => g.id)).then((c) => !cancelled && setCounts(c));
|
||||
void countMembers(groups.map((g) => g.id)).then((c) => !canceled && setCounts(c));
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
if (!canceled) {
|
||||
setPage({ groups: [], total: 0 });
|
||||
setError(describeDirectoryError(err, "group"));
|
||||
}
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
canceled = true;
|
||||
};
|
||||
}, [query, position, reload]);
|
||||
|
||||
@@ -76,13 +76,13 @@ export function GroupsAdmin({ selectedId }: { selectedId?: string }) {
|
||||
setLoose(null);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
let canceled = false;
|
||||
void getGroups([selectedId]).then(
|
||||
([g]) => { if (!cancelled) setLoose(g ?? null); },
|
||||
() => { if (!cancelled) setLoose(null); },
|
||||
([g]) => { if (!canceled) setLoose(g ?? null); },
|
||||
() => { if (!canceled) setLoose(null); },
|
||||
);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
canceled = true;
|
||||
};
|
||||
}, [selectedId, page]);
|
||||
|
||||
|
||||
@@ -42,22 +42,22 @@ export function ListsAdmin({ selectedId }: { selectedId?: string }) {
|
||||
}, [text]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let canceled = false;
|
||||
setError(null);
|
||||
void (async () => {
|
||||
try {
|
||||
const q = await queryLists({ text: query, position, limit: PAGE_SIZE });
|
||||
const lists = await getLists(q.ids);
|
||||
if (!cancelled) setPage({ lists, total: q.total });
|
||||
if (!canceled) setPage({ lists, total: q.total });
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
if (!canceled) {
|
||||
setPage({ lists: [], total: 0 });
|
||||
setError(describeDirectoryError(err, "list"));
|
||||
}
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
canceled = true;
|
||||
};
|
||||
}, [query, position, reload]);
|
||||
|
||||
@@ -70,13 +70,13 @@ export function ListsAdmin({ selectedId }: { selectedId?: string }) {
|
||||
setLoose(null);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
let canceled = false;
|
||||
void getLists([selectedId]).then(
|
||||
([l]) => { if (!cancelled) setLoose(l ?? null); },
|
||||
() => { if (!cancelled) setLoose(null); },
|
||||
([l]) => { if (!canceled) setLoose(l ?? null); },
|
||||
() => { if (!canceled) setLoose(null); },
|
||||
);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
canceled = true;
|
||||
};
|
||||
}, [selectedId, page]);
|
||||
|
||||
|
||||
@@ -29,31 +29,31 @@ export function RolesAdmin({ selectedId }: { selectedId?: string }) {
|
||||
const [reload, setReload] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let canceled = false;
|
||||
setError(null);
|
||||
listAllRoles().then(
|
||||
(list) => !cancelled && setRoles(list),
|
||||
(list) => !canceled && setRoles(list),
|
||||
(err) => {
|
||||
if (cancelled) return;
|
||||
if (canceled) return;
|
||||
setRoles([]);
|
||||
setError(describeDirectoryError(err, "role"));
|
||||
},
|
||||
);
|
||||
void loadRoleDefaults().then((d) => !cancelled && setDefaults(d));
|
||||
void loadRoleDefaults().then((d) => !canceled && setDefaults(d));
|
||||
return () => {
|
||||
cancelled = true;
|
||||
canceled = true;
|
||||
};
|
||||
}, [reload]);
|
||||
|
||||
// The permission list changes only when Stalwart is upgraded; once per visit is plenty.
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let canceled = false;
|
||||
Promise.all([loadPermissionList(), loadPermissionCatalog()]).then(
|
||||
([list, catalog]) => !cancelled && setEntries(describePermissions(list, catalog, t("General"))),
|
||||
(err) => !cancelled && setPermissionsError(t("Stalwart's list of permissions could not be loaded, so permissions can't be changed here. ({reason})", { reason: describeDirectoryError(err, "role") })),
|
||||
([list, catalog]) => !canceled && setEntries(describePermissions(list, catalog, t("General"))),
|
||||
(err) => !canceled && setPermissionsError(t("Stalwart's list of permissions could not be loaded, so permissions can't be changed here. ({reason})", { reason: describeDirectoryError(err, "role") })),
|
||||
);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
canceled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -97,10 +97,10 @@ export function TenantSheet({ tenant, roles, onClose, onChanged, onCreated, onDe
|
||||
|
||||
useEffect(() => {
|
||||
if (!tenant) return;
|
||||
let cancelled = false;
|
||||
void countTenantMembers(tenant.id).then((c) => !cancelled && setCounts(c));
|
||||
let canceled = false;
|
||||
void countTenantMembers(tenant.id).then((c) => !canceled && setCounts(c));
|
||||
return () => {
|
||||
cancelled = true;
|
||||
canceled = true;
|
||||
};
|
||||
}, [tenant, revision]);
|
||||
|
||||
@@ -260,21 +260,21 @@ function TenantDomains({ tenant, canChange, onChanged }: { tenant: DirectoryTena
|
||||
const [revision, setRevision] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let canceled = false;
|
||||
tenantDomains(tenant.id).then(
|
||||
(s) => {
|
||||
if (cancelled) return;
|
||||
if (canceled) return;
|
||||
setState(s);
|
||||
setPick(s.unassigned[0]?.id ?? "");
|
||||
},
|
||||
(err) => {
|
||||
if (cancelled) return;
|
||||
if (canceled) return;
|
||||
setState({ inTenant: [], unassigned: [] });
|
||||
setError(describeDirectoryError(err, "domain"));
|
||||
},
|
||||
);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
canceled = true;
|
||||
};
|
||||
}, [tenant, revision]);
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { TenantSheet } from "./TenantSheet";
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
/**
|
||||
* Tenants: separate organisations on one server, each with its own people,
|
||||
* Tenants: separate organizations on one server, each with its own people,
|
||||
* domains and limits.
|
||||
*
|
||||
* The section is offered to whoever may read tenants. On a server that does not
|
||||
@@ -35,7 +35,7 @@ export function TenantsAdmin({ selectedId }: { selectedId?: string }) {
|
||||
<div className="admin-head">
|
||||
<div className="grow">
|
||||
<h1>{t("Tenants")}</h1>
|
||||
<p className="lead">{t("Separate organisations on one server, each with its own people, domains and limits.")}</p>
|
||||
<p className="lead">{t("Separate organizations on one server, each with its own people, domains and limits.")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<EnterpriseNotice warn />
|
||||
@@ -71,22 +71,22 @@ function EnterpriseTenants({ selectedId, notice }: { selectedId?: string; notice
|
||||
}, [text]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let canceled = false;
|
||||
setError(null);
|
||||
void (async () => {
|
||||
try {
|
||||
const q = await queryTenants({ text: query, position, limit: PAGE_SIZE });
|
||||
const tenants = await getTenants(q.ids);
|
||||
if (!cancelled) setPage({ tenants, total: q.total });
|
||||
if (!canceled) setPage({ tenants, total: q.total });
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
if (!canceled) {
|
||||
setPage({ tenants: [], total: 0 });
|
||||
setError(describeDirectoryError(err, "tenant"));
|
||||
}
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
canceled = true;
|
||||
};
|
||||
}, [query, position, reload]);
|
||||
|
||||
@@ -99,13 +99,13 @@ function EnterpriseTenants({ selectedId, notice }: { selectedId?: string; notice
|
||||
setLoose(null);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
let canceled = false;
|
||||
void getTenants([selectedId]).then(
|
||||
([x]) => { if (!cancelled) setLoose(x ?? null); },
|
||||
() => { if (!cancelled) setLoose(null); },
|
||||
([x]) => { if (!canceled) setLoose(x ?? null); },
|
||||
() => { if (!canceled) setLoose(null); },
|
||||
);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
canceled = true;
|
||||
};
|
||||
}, [selectedId, page]);
|
||||
|
||||
@@ -118,7 +118,7 @@ function EnterpriseTenants({ selectedId, notice }: { selectedId?: string; notice
|
||||
<div className="admin-head">
|
||||
<div className="grow">
|
||||
<h1>{t("Tenants")}</h1>
|
||||
<p className="lead">{t("Separate organisations on one server, each with its own people, domains and limits.")}</p>
|
||||
<p className="lead">{t("Separate organizations on one server, each with its own people, domains and limits.")}</p>
|
||||
</div>
|
||||
{can(perms, "Tenant", "Create") && (
|
||||
<button className="btn btn-primary" onClick={() => navigate("/admin/tenants/new")}>
|
||||
|
||||
Reference in New Issue
Block a user