refactor(ui): address code review - optimize queries, extract helpers, improve error handling
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
+21
-12
@@ -63,18 +63,27 @@ async def integrations_dashboard(request: Request, db: Session = Depends(get_db)
|
||||
tier_id = "free"
|
||||
|
||||
if owner_id:
|
||||
integrations = (
|
||||
db.query(UserIntegration)
|
||||
.filter(UserIntegration.owner_id == owner_id)
|
||||
.order_by(UserIntegration.id)
|
||||
.all()
|
||||
)
|
||||
dest_count = sum(1 for i in integrations if i.direction == IntegrationDirection.DESTINATION)
|
||||
src_count = sum(
|
||||
1
|
||||
for i in integrations
|
||||
if i.direction == IntegrationDirection.SOURCE and i.integration_type in _MAILBOX_SOURCE_TYPES
|
||||
)
|
||||
from sqlalchemy import func
|
||||
|
||||
dest_count = (
|
||||
db.query(func.count())
|
||||
.select_from(UserIntegration)
|
||||
.filter(
|
||||
UserIntegration.owner_id == owner_id,
|
||||
UserIntegration.direction == IntegrationDirection.DESTINATION,
|
||||
)
|
||||
.scalar()
|
||||
) or 0
|
||||
src_count = (
|
||||
db.query(func.count())
|
||||
.select_from(UserIntegration)
|
||||
.filter(
|
||||
UserIntegration.owner_id == owner_id,
|
||||
UserIntegration.direction == IntegrationDirection.SOURCE,
|
||||
UserIntegration.integration_type.in_(list(_MAILBOX_SOURCE_TYPES)),
|
||||
)
|
||||
.scalar()
|
||||
) or 0
|
||||
tier_id = get_user_tier_id(db, owner_id)
|
||||
tier = get_tier(tier_id, db)
|
||||
tier_name = tier.get("name", tier_id)
|
||||
|
||||
@@ -140,6 +140,39 @@ In addition to the system-wide IMAP mailboxes configured by the administrator vi
|
||||
|
||||
The quota bar on the Email Ingestion page shows your current usage against your plan limit. If you have reached the limit, delete an existing account or upgrade your plan.
|
||||
|
||||
#### Integrations Dashboard
|
||||
|
||||
The **Integrations** page (`/integrations`) provides a unified view of all your configured ingestion sources and storage destinations. Instead of managing each integration type separately, you can create, edit, test, and delete any integration from a single dashboard.
|
||||
|
||||
**Opening the dashboard:** Click **Integrations** in the top navigation bar.
|
||||
|
||||
**Quota indicators** at the top of the page show your current usage:
|
||||
- **Mailbox Sources** — how many IMAP ingestion accounts you have vs. your plan limit
|
||||
- **Storage Destinations** — how many storage targets you have vs. your plan limit
|
||||
- An **Upgrade Plan** link appears when you have reached your plan limit
|
||||
|
||||
**Adding a new integration:**
|
||||
|
||||
1. Click **Add Integration**.
|
||||
2. Choose a **Direction** — Source (ingestion) or Destination (storage).
|
||||
3. Choose an **Integration Type** (e.g. IMAP, S3, Dropbox, WebDAV).
|
||||
4. Fill in the type-specific fields — the form adapts dynamically based on your choice:
|
||||
- **IMAP** — host, port, username, password, SSL toggle
|
||||
- **S3** — bucket, region, access key, secret key
|
||||
- **WebDAV / Nextcloud** — URL, folder, username, password
|
||||
- **FTP / SFTP** — host, port, remote path, username, password
|
||||
- **Dropbox / Google Drive / OneDrive** — folder path, with a link to the OAuth setup page
|
||||
- **Email Forward** — recipient email address
|
||||
- **Watch Folder** — folder path
|
||||
- **Paperless NGX** — URL and API token
|
||||
5. Click **Test Connection** to verify the settings before saving.
|
||||
6. Click **Save** to persist the integration.
|
||||
|
||||
**Managing existing integrations:**
|
||||
- Click **Test** on any card to re-verify the connection.
|
||||
- Click **Edit** to update the configuration or credentials.
|
||||
- Click **Delete** to permanently remove the integration.
|
||||
|
||||
### Watch Folders (Automatic Folder Ingestion)
|
||||
|
||||
Watch folders allow DocuElevate to automatically monitor directories for new files and ingest them without any manual action.
|
||||
|
||||
@@ -610,7 +610,7 @@
|
||||
</template>
|
||||
|
||||
<!-- Generic fallback for types without dedicated fields -->
|
||||
<template x-if="form.integration_type && !['IMAP','S3','WEBDAV','NEXTCLOUD','FTP','SFTP','DROPBOX','GOOGLE_DRIVE','ONEDRIVE','EMAIL','WATCH_FOLDER','PAPERLESS'].includes(form.integration_type)">
|
||||
<template x-if="form.integration_type && !hasFormFields(form.integration_type)">
|
||||
<div class="space-y-3 border-t border-gray-200 dark:border-gray-700 pt-3">
|
||||
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider" x-text="form.integration_type + ' Settings'"></p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
@@ -741,6 +741,7 @@
|
||||
function integrationsDashboard() {
|
||||
const SOURCE_TYPES = ['IMAP', 'WATCH_FOLDER', 'WEBHOOK'];
|
||||
const DEST_TYPES = ['S3', 'DROPBOX', 'GOOGLE_DRIVE', 'ONEDRIVE', 'WEBDAV', 'NEXTCLOUD', 'FTP', 'SFTP', 'EMAIL', 'PAPERLESS', 'RCLONE'];
|
||||
const TYPES_WITH_FORM_FIELDS = new Set(['IMAP', 'S3', 'WEBDAV', 'NEXTCLOUD', 'FTP', 'SFTP', 'DROPBOX', 'GOOGLE_DRIVE', 'ONEDRIVE', 'EMAIL', 'WATCH_FOLDER', 'PAPERLESS']);
|
||||
|
||||
const TYPE_LABELS = {
|
||||
IMAP: 'IMAP Email',
|
||||
@@ -836,6 +837,7 @@ function integrationsDashboard() {
|
||||
|
||||
typeLabel(t) { return TYPE_LABELS[t] || t; },
|
||||
typeIcon(t) { return TYPE_ICONS[t] || 'fa-plug text-gray-400'; },
|
||||
hasFormFields(t) { return TYPES_WITH_FORM_FIELDS.has(t); },
|
||||
|
||||
oauthLink(t) {
|
||||
if (t === 'DROPBOX') return '/dropbox';
|
||||
@@ -982,13 +984,13 @@ function integrationsDashboard() {
|
||||
if (payload.credentials) update.credentials = payload.credentials;
|
||||
resp = await fetch(`/api/integrations/${this.editingIntegration.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken() },
|
||||
headers: authHeaders(true),
|
||||
body: JSON.stringify(update),
|
||||
});
|
||||
} else {
|
||||
resp = await fetch('/api/integrations/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken() },
|
||||
headers: authHeaders(true),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
@@ -1025,7 +1027,7 @@ function integrationsDashboard() {
|
||||
};
|
||||
const resp = await fetch('/api/integrations/test', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken() },
|
||||
headers: authHeaders(true),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
this.testResult = await resp.json();
|
||||
@@ -1041,10 +1043,14 @@ function integrationsDashboard() {
|
||||
try {
|
||||
// Retrieve saved credentials to test
|
||||
const credsResp = await fetch(`/api/integrations/${intg.id}/credentials`);
|
||||
const creds = credsResp.ok ? await credsResp.json() : null;
|
||||
if (!credsResp.ok) {
|
||||
this.showAlert('error', 'Test Failed', 'Could not retrieve saved credentials for testing.');
|
||||
return;
|
||||
}
|
||||
const creds = await credsResp.json();
|
||||
const resp = await fetch('/api/integrations/test', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken() },
|
||||
headers: authHeaders(true),
|
||||
body: JSON.stringify({
|
||||
integration_type: intg.integration_type,
|
||||
config: intg.config,
|
||||
@@ -1075,7 +1081,7 @@ function integrationsDashboard() {
|
||||
try {
|
||||
const resp = await fetch(`/api/integrations/${this.integrationToDelete.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'X-CSRF-Token': getCsrfToken() },
|
||||
headers: authHeaders(false),
|
||||
});
|
||||
if (resp.ok || resp.status === 204) {
|
||||
this.integrations = this.integrations.filter(i => i.id !== this.integrationToDelete.id);
|
||||
@@ -1112,5 +1118,11 @@ function getCsrfToken() {
|
||||
if (meta) return meta.getAttribute('content');
|
||||
return '';
|
||||
}
|
||||
|
||||
function authHeaders(json) {
|
||||
const h = { 'X-CSRF-Token': getCsrfToken() };
|
||||
if (json) h['Content-Type'] = 'application/json';
|
||||
return h;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user