Skip to content
Merged
57 changes: 57 additions & 0 deletions src/apps/admin/mailbrokermanagerapp/MailbrokerManager.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<script lang="ts">
import type { ICommandResult } from "$interfaces/ICommandResult";
import type { IMailbrokerManagerRuntime } from "$interfaces/runtimes/IMailbrokerRuntime";
import CustomTitlebar from "$lib/CustomTitlebar.svelte";
import { CommandResult } from "$ts/result";
import { onMount, type Component } from "svelte";
import Sidebar from "./MailbrokerManager/Sidebar.svelte";
import { mailbrokerPages } from "./store";
import type { MailbrokerPage } from "./types";
import Spinner from "$lib/Spinner.svelte";

let { process }: { process: IMailbrokerManagerRuntime } = $props();
const { currentPage, pageProps } = process;
let Page: Component | undefined = $state();
let pageData = $state<MailbrokerPage>();
let loading = $state<boolean>(false);
let data = $state<ICommandResult<Record<string, any>>>();

onMount(() => {
const sub = currentPage.subscribe(async (v) => {
loading = true;
pageData = mailbrokerPages.get(v);
const canAccess = !pageData?.scopes || process.admin.canAccess(...pageData?.scopes);
data = canAccess
? ((await pageData?.data?.(process, pageProps())) ?? CommandResult.Ok())
: CommandResult.Error("You're missing the required scopes to access this resource.");

Page = pageData?.content;
loading = false;
});

return () => sub();
});
</script>

<Sidebar {process} />
<div class="container">
<CustomTitlebar {process} />
<div class="page-content page-{pageData?.name.toLowerCase().replaceAll(' ', '-')}" class:loading>
{#if loading}
<Spinner height={32} />
{:else if Page && data?.success}
{@const result = data.result!}
<Page {process} data={result} pageProps={pageProps()} {pageData} />
{:else}
<div class="page-load-error">
<div class="error">
<span class="lucide icon-circle-x"></span>
<div>
<h1>The page failed to load</h1>
<p>{data?.errorMessage ?? "An unknown error occurred."}</p>
</div>
</div>
</div>
{/if}
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script lang="ts">
import { formatBytes } from "$ts/util/fs";
import type { Mailbroker } from "$types/server/mailbroker";

let { sentRecord }: { sentRecord: Mailbroker.SentMail } = $props();
</script>

<div class="pills">
<div class="pill size">
<span class="lucide icon-code"></span>
<span>{formatBytes(sentRecord.htmlContent.length)}</span>
</div>
<div class="pill size">
<span class="lucide icon-text-align-start"></span>
<span>{formatBytes(sentRecord.textContent.length)}</span>
</div>
{#if sentRecord.to.username && sentRecord.to.serverName}
<div class="pill clr-blue">
<span class="lucide icon-user-round"></span>
<span>To a user</span>
</div>
{/if}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script lang="ts">
import { Server } from "$ts/env";
import type { Mailbroker } from "$types/server/mailbroker";

let { serverKey, disabled = false }: { serverKey: Mailbroker.MailKey; disabled: boolean } = $props();
</script>

<div class="pills">
{#if disabled}
<div class="pill clr-orange">
<span>Disabled</span>
</div>
{:else}
<div class="pill clr-green">
<span>Enabled</span>
</div>
{/if}
{#if serverKey.serverName === "arcapi.nl"}
<div class="pill clr-purple">Master key</div>
{/if}
{#if Server.hostname === serverKey.serverName}
<div class="pill clr-blue">This server</div>
{/if}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script lang="ts">
import type { IMailbrokerManagerRuntime } from "$interfaces/runtimes/IMailbrokerRuntime";
import { mailbrokerPages } from "../store";

let { process }: { process: IMailbrokerManagerRuntime } = $props();
const { currentPage } = process;
</script>

<div class="window-symbolic-sidebar">
<section class="sidebar-section">
<h1>Mailbroker manager</h1>
{#each [...mailbrokerPages] as [id, page]}
{#if !page.hidden}
<button class="sidebar-button page" class:selected={$currentPage === id} onclick={() => process.switchPage(id)}>
<span class="lucide icon-{page.icon}"></span>
<span>{page.name}</span>
</button>
{#if page.separator}
<hr />
{/if}
{/if}
{/each}
</section>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<script lang="ts">
import type { IMailbrokerManagerRuntime } from "$interfaces/runtimes/IMailbrokerRuntime";
import { AdminScopes } from "$ts/servicehost/services/AdminBootstrapper/store";
import { Plural } from "$ts/util";
import { Store } from "$ts/writable";
import type { Mailbroker } from "$types/server/mailbroker";
import type { Unsubscriber } from "$types/shared/writable";
import { onDestroy, onMount } from "svelte";
import ListingOption from "./TemplateListing/ListingOption.svelte";

const {
process,
templates,
hideNewButton = false,
}: {
process: IMailbrokerManagerRuntime;
templates: Mailbroker.MailTemplate[];
hideNewButton?: boolean;
} = $props();
const searchValue = Store<string>("");

let filteredTemplates = $state<Mailbroker.MailTemplate[]>(templates);
let unsubscribe: Unsubscriber;

onMount(() => {
unsubscribe = searchValue.subscribe((v) => {
const query = v.toLowerCase().trim();

if (!query) {
filteredTemplates = templates;
return;
}

filteredTemplates = templates.filter((template) => {
if (template.name.toLowerCase().includes(query)) return true;
if (template.fromSuffix.toLowerCase().includes(query)) return true;
if (template.subjectContent.toLowerCase().includes(query)) return true;
if (template.textContent.toLowerCase().includes(query)) return true;
if (template._id.toLowerCase().includes(query)) return true;

return false;
});
});
});

onDestroy(() => {
unsubscribe?.();
});
</script>

<div class="template-listing">
<div class="listing-header">
<h1>{filteredTemplates.length} {Plural("template", filteredTemplates.length)}</h1>
<div class="search-bar">
<span class="lucide icon-search"> </span>
<input type="text" bind:value={$searchValue} placeholder="Search templates" />
</div>
{#if !hideNewButton && process.admin.canAccess(AdminScopes.adminMailbrokerTemplatesWrite)}
<button class="suggested new-template" onclick={() => process.switchPage("newTemplate")}>
<span>New...</span>
</button>
{/if}
</div>

<div class="templates">
{#each filteredTemplates as template (template._id)}
<ListingOption {process} {template} />
{/each}

{#if !filteredTemplates.length}
<p class="empty-notice">
<span>There are no templates that match the criteria.</span>
</p>
{/if}
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script lang="ts">
import type { IMailbrokerManagerRuntime } from "$interfaces/runtimes/IMailbrokerRuntime";
import type { Mailbroker } from "$types/server/mailbroker";
import TemplatePills from "../TemplatePills.svelte";

let { template, process }: { template: Mailbroker.MailTemplate; process: IMailbrokerManagerRuntime } = $props();

async function viewTemplate() {
process.switchPage("viewTemplate", { templateId: template._id });
}
</script>

<button class="template-listing-option" onclick={viewTemplate}>
<span class="lucide icon-book-dashed"></span>
<div class="info">
<h2>{template.name} - ArcOS {template.fromSuffix}</h2>
<p class="subject">Subject: {template.subjectContent}</p>
<TemplatePills {template} deprecated={template.deprecated} />
</div>
</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script lang="ts">
import { formatBytes } from "$ts/util/fs";
import type { Mailbroker } from "$types/server/mailbroker";

let { template, deprecated }: { template: Mailbroker.MailTemplate; deprecated: boolean; } = $props();
</script>

<div class="pills">
<div class="pill size">
<span class="lucide icon-code"></span>
<span>{formatBytes(template.htmlContent.length)}</span>
</div>
<div class="pill size">
<span class="lucide icon-text-align-start"></span>
<span>{formatBytes(template.textContent.length)}</span>
</div>
{#if deprecated}
<div class="pill clr-orange">Deprecated</div>
{/if}
</div>
44 changes: 44 additions & 0 deletions src/apps/admin/mailbrokermanagerapp/MailbrokerManagerApp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import "$css/apps/admin/mailbroker.css";
import type { App } from "$types/apps/app";
import MailbrokerManager from "./MailbrokerManager.svelte";
import { MailbrokerManagerRuntime } from "./runtime";

const MailbrokerManagerApp: App = {
metadata: {
name: "Mailbroker Manager",
version: "1.0.0",
author: "Izaak Kuipers",
appGroup: "adminTools",
icon: "MailbrokerAdminIcon",
},
position: { centered: true },
size: { w: 800, h: 480 },
minSize: { w: 800, h: 480 },
maxSize: { w: NaN, h: NaN },
state: {
maximized: false,
minimized: false,
resizable: true,
fullscreen: false,
headless: true,
},
controls: {
minimize: true,
maximize: true,
close: true,
},
assets: {
runtime: MailbrokerManagerRuntime,
component: MailbrokerManager as any,
},
glass: true,
elevated: true,
hidden: false,
core: false,
overlay: false,
noSafeMode: false,
vital: true,
id: "MailbrokerManagerApp",
};

export default MailbrokerManagerApp;
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<script lang="ts">
import type { IMailbrokerNewKeyOverlayRuntime } from "$interfaces/runtimes/IMailbrokerRuntime";
import Icon from "$lib/Icon.svelte";
import ActionBar from "$lib/Window/ActionBar.svelte";
import ActionButton from "$lib/Window/ActionBar/ActionButton.svelte";
import { BTN_OKAY_SUG, MessageBox } from "$ts/util/dialog";

let { process }: { process: IMailbrokerNewKeyOverlayRuntime } = $props();
let serverName = $state("");

async function create() {
const result = await process.admin.createMailbrokerKey(serverName);

if (!result.success) {
MessageBox(
{
title: "Failed to create server key",
message: `An error occurred while attempting to create the mailbroker key for ${serverName}. ${result.errorMessage ?? "Unknown failure"}`,
image: "ErrorIcon",
sound: "arcos.dialog.warning",
buttons: [BTN_OKAY_SUG],
},
process.parentPid, // the overlay is too small, so use parent
true
);

return;
}

await process.closeWindow();
await process.parent.spawnOverlay("KeyOverlay", result.result!._id);
}
</script>

<div class="top">
<Icon icon="MailbrokerAdminIcon"></Icon>
<div class="right">
<h1>New server key</h1>
<p>Enter the name of the server for which you want to create the server key:</p>
<input type="text" bind:value={serverName} placeholder="*.arcapi.nl" />
</div>
</div>

<ActionBar>
{#snippet rightContent()}
<ActionButton onclick={() => process.closeWindow()}>Cancel</ActionButton>
<ActionButton suggested disabled={!serverName} onclick={create}>Create</ActionButton>
{/snippet}
</ActionBar>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { App } from "$types/apps/app";
import NewKey from "./NewKey.svelte";
import { MailbrokerNewKeyOverlayRuntime } from "./runtime";

const NewKeyOverlay: App = {
metadata: {
name: "New mailbroker key",
author: "Izaak Kuipers",
version: "1.0.0",
icon: "MailbrokerAdminIcon",
},
position: { centered: true },
size: {
w: 380,
h: 200,
},
minSize: {
w: 380,
h: 200,
},
maxSize: {
w: 380,
h: 200,
},
controls: {
minimize: false,
maximize: false,
close: true,
},
state: {
resizable: false,
headless: true,
fullscreen: false,
maximized: false,
minimized: false,
},
assets: {
runtime: MailbrokerNewKeyOverlayRuntime,
component: NewKey as any,
},
hidden: true,
id: "MailbrokerManagerApp_NewKeyOverlay",
};

export default NewKeyOverlay;
Loading
Loading