AGPL source offer and name cleanup
Every build writes the exact source it was built from, uncommitted work and new files included, as source.tar.gz next to the app, named after that tree. Docker builds, which have no git, pack the build context and name it by a hash of its files. The sign-in page and Settings > About link to it instead of a repository that can drift. What users, operators and packagers see no longer names the upstream server: - interface text, in all nine catalogues, with a token-session line for Security; - server messages; - the settings, now MAIL_SERVER_URL, MAIL_SERVERS_FILE, ADMIN_URL and MAIL_SERVER_FOLLOW_ADVERTISED_URLS, and mail-servers.example.json; - the Tenants notice, which is gone; - the README, CONTRIBUTING and SECURITY. ihasmail's own FEATURES, KNOWN-ISSUES and ROADMAP stay with public ihasmail, and INBUXA.md is folded into the README.
This commit is contained in:
@@ -115,12 +115,12 @@ export function AdminDashboard() {
|
||||
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.")}
|
||||
{t("Detailed metrics, the delivery queue, logs and server settings are in INBUXA Admin.")}
|
||||
{adminUrl && (
|
||||
<>
|
||||
{" "}
|
||||
<a href={adminUrl} target="_blank" rel="noopener noreferrer">
|
||||
{t("Open Stalwart admin")} <ExternalLink size={13} aria-hidden="true" />
|
||||
{t("Open INBUXA Admin")} <ExternalLink size={13} aria-hidden="true" />
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -153,7 +153,7 @@ export function RoleSheet({ role, roles, defaults, entries, permissionsError, on
|
||||
{!creating && !locked && !can(perms, "Role", "Update") && <p className="admin-notice">{t("Your role lets you view roles but not change them.")}</p>}
|
||||
{kinds.length > 0 && (
|
||||
<p className="admin-notice warn">
|
||||
<span>{t("Stalwart gives this role by default to {kinds}. A change here reaches everyone who has it that way.", { kinds: kinds.join(", ") })}</span>
|
||||
<span>{t("The mail server gives this role by default to {kinds}. A change here reaches everyone who has it that way.", { kinds: kinds.join(", ") })}</span>
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -186,7 +186,7 @@ export function RoleSheet({ role, roles, defaults, entries, permissionsError, on
|
||||
{!creating && can(perms, "Role", "Destroy") && (
|
||||
<DeleteRole
|
||||
role={role}
|
||||
blocked={kinds.length ? t("Stalwart gives this role by default, so it can't be deleted. Change the defaults in Stalwart's own administration first.") : locked ? t("This role carries permissions yours doesn't.") : null}
|
||||
blocked={kinds.length ? t("The mail server gives this role by default, so it can't be deleted. Change the defaults in INBUXA Admin first.") : locked ? t("This role carries permissions yours doesn't.") : null}
|
||||
onDeleted={onDeleted}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -50,7 +50,7 @@ export function RolesAdmin({ selectedId }: { selectedId?: string }) {
|
||||
let canceled = false;
|
||||
Promise.all([loadPermissionList(), loadPermissionCatalog()]).then(
|
||||
([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") })),
|
||||
(err) => !canceled && setPermissionsError(t("The mail server's list of permissions could not be loaded, so permissions can't be changed here. ({reason})", { reason: describeDirectoryError(err, "role") })),
|
||||
);
|
||||
return () => {
|
||||
canceled = true;
|
||||
|
||||
@@ -176,7 +176,7 @@ export function TenantSheet({ tenant, roles, onClose, onChanged, onCreated, onDe
|
||||
<div className="field">
|
||||
<label htmlFor="admin-tenant-logo">{t("Logo")}</label>
|
||||
<input id="admin-tenant-logo" className="input" value={logo} disabled={!editable} placeholder="https://…" spellCheck={false} onChange={(e) => setLogo(e.target.value)} />
|
||||
<span className="hint">{t("An https address or a data URL of an image. Stalwart shows it to the tenant's people where it shows a logo.")}</span>
|
||||
<span className="hint">{t("An https address or a data URL of an image. The mail server shows it to the tenant's people where it shows a logo.")}</span>
|
||||
</div>
|
||||
|
||||
{!creating && (
|
||||
@@ -215,7 +215,7 @@ export function TenantSheet({ tenant, roles, onClose, onChanged, onCreated, onDe
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="hint">{t("Stalwart refuses to create more than a limit allows. An empty field is no limit.")}</p>
|
||||
<p className="hint">{t("The mail server refuses to create more than a limit allows. An empty field is no limit.")}</p>
|
||||
|
||||
<h3>{t("Role")}</h3>
|
||||
<select className="input admin-wide" aria-label={t("Role")} value={role} disabled={!editable} onChange={(e) => setRole(e.target.value)}>
|
||||
|
||||
@@ -7,7 +7,6 @@ import { drawableLogo, getTenants, queryTenants, type DirectoryTenant } from "@/
|
||||
import { formatSize } from "@/lib/format";
|
||||
import { proxiedImageUrl } from "@/lib/text/html";
|
||||
import { plural, t } from "@/lib/i18n";
|
||||
import { useSession } from "@/store/session";
|
||||
import { Empty, Spinner } from "@/ui/misc";
|
||||
import { usePermissions } from "./usePermissions";
|
||||
import { TenantSheet } from "./TenantSheet";
|
||||
@@ -18,23 +17,15 @@ const PAGE_SIZE = 50;
|
||||
* Tenants: separate organizations on one server, each with its own people,
|
||||
* domains and limits.
|
||||
*
|
||||
* The section is offered to whoever may read tenants. INBUXA ships tenants to
|
||||
* everybody, whatever edition the server reports, so there is no edition
|
||||
* check here (public ihasmail shows only a notice unless the server reports
|
||||
* Enterprise). SHOW_ENTERPRISE_NOTICES still adds the notice, for talking to
|
||||
* upstream Stalwart.
|
||||
* The section is offered to whoever may read tenants. The INBUXA mail server
|
||||
* has one edition with tenants in it, so there is no edition check and no
|
||||
* notice here, unlike public ihasmail.
|
||||
*/
|
||||
export function TenantsAdmin({ selectedId }: { selectedId?: string }) {
|
||||
const notices = useSession((s) => s.session?.ihasmail?.server?.enterpriseNotices === true);
|
||||
return <EnterpriseTenants selectedId={selectedId} notice={notices} />;
|
||||
return <EnterpriseTenants selectedId={selectedId} />;
|
||||
}
|
||||
|
||||
/** Said on every Tenants page, Enterprise or not. */
|
||||
function EnterpriseNotice({ warn }: { warn: boolean }) {
|
||||
return <p className={`admin-notice${warn ? " warn" : ""}`}>{t("Tenants are a Stalwart Enterprise feature.")}</p>;
|
||||
}
|
||||
|
||||
function EnterpriseTenants({ selectedId, notice }: { selectedId?: string; notice: boolean }) {
|
||||
function EnterpriseTenants({ selectedId }: { selectedId?: string }) {
|
||||
const [, navigate] = useLocation();
|
||||
const perms = usePermissions();
|
||||
const [text, setText] = useState("");
|
||||
@@ -111,7 +102,6 @@ function EnterpriseTenants({ selectedId, notice }: { selectedId?: string; notice
|
||||
)}
|
||||
</div>
|
||||
|
||||
{notice && <EnterpriseNotice warn={false} />}
|
||||
|
||||
<div className="admin-toolbar">
|
||||
<label className="admin-search">
|
||||
|
||||
@@ -112,7 +112,7 @@ describe("the Administration dashboard", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("the pointer to Stalwart's own administration", () => {
|
||||
describe("the pointer to INBUXA Admin", () => {
|
||||
let host: HTMLDivElement;
|
||||
let root: Root;
|
||||
beforeEach(() => {
|
||||
@@ -136,7 +136,7 @@ describe("the pointer to Stalwart's own administration", () => {
|
||||
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");
|
||||
expect(note.textContent).toContain("INBUXA Admin");
|
||||
const link = note.querySelector("a")!;
|
||||
expect(link.getAttribute("href")).toBe("https://admin.example.com");
|
||||
expect(link.getAttribute("rel")).toBe("noopener noreferrer");
|
||||
@@ -144,7 +144,7 @@ describe("the pointer to Stalwart's own administration", () => {
|
||||
|
||||
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")?.textContent).toContain("INBUXA Admin");
|
||||
expect(host.querySelector(".admin-dashboard-note a")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -86,7 +86,7 @@ describe("the role sheet", () => {
|
||||
it("warns about a default role and will not delete it", async () => {
|
||||
signIn(VIEWER);
|
||||
await render(roles.get("user")!, { user: ["user"], group: [], tenant: [], admin: [] });
|
||||
expect(host.textContent).toContain("Stalwart gives this role by default to users");
|
||||
expect(host.textContent).toContain("The mail server gives this role by default to users");
|
||||
expect(button(host, "Delete role…")?.disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,15 +69,14 @@ describe("the Tenants page where the installation asks for Enterprise notices",
|
||||
host.remove();
|
||||
});
|
||||
|
||||
it("says tenants are Enterprise above the list, as the demo does", async () => {
|
||||
it("shows no Enterprise notice, even where the installation asks for one", async () => {
|
||||
signIn("enterprise", true);
|
||||
const { hook } = memoryLocation({ path: "/admin/tenants" });
|
||||
await act(async () => {
|
||||
root.render(<Router hook={hook}><TenantsAdmin /></Router>);
|
||||
});
|
||||
await act(async () => {});
|
||||
expect(host.querySelector(".admin-notice")?.textContent).toBe("Tenants are a Stalwart Enterprise feature.");
|
||||
expect(host.querySelector(".admin-notice.warn")).toBeNull();
|
||||
expect(host.querySelector(".admin-notice")).toBeNull();
|
||||
expect(host.querySelector(".admin-table")?.textContent).toContain("Acme Corp");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user