Packages

phoenix_kit

1.7.78
1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib modules storage web settings.html.heex
Raw

lib/modules/storage/web/settings.html.heex

<PhoenixKitWeb.Components.LayoutWrapper.app_layout
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title="Storage Settings"
current_path={@current_path}
current_locale={@current_locale}
project_title={@project_title}
>
<div class="container mx-auto px-4 py-8 max-w-6xl">
<.admin_page_header
back={PhoenixKit.Utils.Routes.path("/admin/modules")}
title="Storage Settings"
subtitle="Configure storage buckets and file processing settings for your application"
/>
<%!-- Module Status Alert --%>
<div class="alert alert-info mb-6">
<.icon name="hero-information-circle" class="w-5 h-5" />
<div>
<h3 class="font-bold">Core System Module</h3>
<p class="text-sm">
Storage is a core system module that is always enabled and cannot be disabled.
It provides essential file management functionality across PhoenixKit.
</p>
</div>
</div>
<%!-- Repair Storage Module Section --%>
<div class="alert alert-warning mb-6">
<.icon name="hero-wrench-screwdriver" class="w-5 h-5" />
<div class="flex-1">
<h3 class="font-bold">Storage Module Repair</h3>
<p class="text-sm">
If your storage module isn't working correctly, use this to reset configuration to defaults.
This will NOT delete any uploaded files.
</p>
</div>
<button
phx-click="repair_storage_module"
data-confirm="This will reset dimensions and settings to defaults, and create a local bucket if none exist. Your uploaded files will NOT be deleted. Continue?"
class="btn btn-warning btn-sm"
>
<.icon name="hero-wrench-screwdriver" class="w-4 h-4 mr-1" /> Repair Storage Module
</button>
</div>
<%!-- System Dependencies Check --%>
<%= if match?({:error, :not_installed}, @imagemagick_status) do %>
<div class="alert alert-error mb-6">
<.icon name="hero-exclamation-circle" class="w-5 h-5" />
<div>
<h3 class="font-bold">ImageMagick Not Installed</h3>
<p class="text-sm">
ImageMagick is required for image processing and variant generation.
Without it, image resizing, cropping, and format conversion will not work.
</p>
<p class="text-sm mt-2">
<strong>Installation:</strong>
<br />macOS:
<code class="bg-base-200 px-2 py-1 rounded text-xs">brew install imagemagick</code>
<br />Ubuntu/Debian:
<code class="bg-base-200 px-2 py-1 rounded text-xs">
sudo apt-get install imagemagick
</code>
<br />CentOS/RHEL:
<code class="bg-base-200 px-2 py-1 rounded text-xs">
sudo yum install ImageMagick
</code>
</p>
</div>
</div>
<% end %>
<%= if match?({:error, :not_installed}, @ffmpeg_status) do %>
<div class="alert alert-warning mb-6">
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
<div>
<h3 class="font-bold">FFmpeg Not Installed</h3>
<p class="text-sm">
FFmpeg is required for video processing, transcoding, and thumbnail generation.
Without it, video file processing will not work.
</p>
<p class="text-sm mt-2">
<strong>Installation:</strong>
<br />macOS:
<code class="bg-base-200 px-2 py-1 rounded text-xs">brew install ffmpeg</code>
<br />Ubuntu/Debian:
<code class="bg-base-200 px-2 py-1 rounded text-xs">sudo apt-get install ffmpeg</code>
<br />CentOS/RHEL:
<code class="bg-base-200 px-2 py-1 rounded text-xs">sudo yum install ffmpeg</code>
</p>
</div>
</div>
<% end %>
<%!-- Storage Buckets --%>
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<div class="flex justify-between items-center mb-4">
<h2 class="card-title text-2xl">
<.icon name="hero-inbox-stack" class="w-6 h-6 mr-2" /> Storage Buckets
</h2>
<.link
navigate={PhoenixKit.Utils.Routes.path("/admin/settings/media/buckets/new")}
class="btn btn-primary"
>
<.icon name="hero-plus" class="w-4 h-4 mr-1" /> Add Bucket
</.link>
</div>
<%= if Enum.empty?(@buckets) do %>
<div class="alert alert-warning">
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
<div>
<h3 class="font-bold">No Storage Buckets Configured</h3>
<p class="text-sm">
You need to create at least one storage bucket before files can be uploaded.
Buckets can be local directories or cloud storage providers (AWS S3, Backblaze B2, Cloudflare R2).
</p>
<div class="mt-3">
<.link
navigate={PhoenixKit.Utils.Routes.path("/admin/settings/media/buckets/new")}
class="btn btn-warning btn-sm"
>
<.icon name="hero-plus" class="w-4 h-4 mr-1" /> Create Your First Bucket
</.link>
</div>
</div>
</div>
<% else %>
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th>Name</th>
<th>Provider</th>
<th>Priority</th>
<th>Files</th>
<th>Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<%= for bucket <- @buckets do %>
<tr>
<%!-- Name --%>
<td>
<div class="flex items-center gap-2">
<%= case bucket.provider do %>
<% "local" -> %>
<.icon name="hero-folder" class="w-5 h-5 text-primary" />
<% "s3" -> %>
<.icon name="hero-cloud" class="w-5 h-5 text-warning" />
<% "b2" -> %>
<.icon name="hero-cube" class="w-5 h-5 text-info" />
<% "r2" -> %>
<.icon name="hero-cloud-arrow-up" class="w-5 h-5 text-accent" />
<% _ -> %>
<.icon name="hero-server" class="w-5 h-5 text-base-content/50" />
<% end %>
<div>
<div class="font-bold">{bucket.name}</div>
<div class="text-xs text-base-content/60 font-mono">
{get_bucket_full_path(bucket)}
</div>
</div>
</div>
</td>
<%!-- Provider --%>
<td>
<span class="badge badge-ghost badge-sm h-auto">
{String.upcase(bucket.provider)}
</span>
</td>
<%!-- Priority --%>
<td>
<%= if bucket.priority == 0 do %>
<span class="text-xs text-base-content/60">Random (0)</span>
<% else %>
<span class="font-semibold">{bucket.priority}</span>
<% end %>
</td>
<%!-- Files Count --%>
<td>
<span class="text-sm font-semibold">
{Map.get(@bucket_file_counts, bucket.uuid, 0)} files
</span>
</td>
<%!-- Status --%>
<td>
<%= if bucket.enabled do %>
<span class="badge badge-success badge-sm h-auto">Enabled</span>
<% else %>
<span class="badge badge-error badge-sm h-auto">Disabled</span>
<% end %>
</td>
<%!-- Actions --%>
<td>
<div class="flex justify-end gap-1">
<%!-- Edit Button --%>
<.link
navigate={
PhoenixKit.Utils.Routes.path(
"/admin/settings/media/buckets/#{bucket.uuid}/edit"
)
}
class="btn btn-xs btn-outline btn-info tooltip tooltip-bottom"
data-tip={gettext("Edit bucket")}
>
<.icon name="hero-pencil" class="w-4 h-4 hidden sm:inline" />
<span class="sm:hidden whitespace-nowrap">
{gettext("Edit bucket")}
</span>
</.link>
<%!-- Toggle Enable/Disable --%>
<button
phx-click="toggle_bucket"
phx-value-id={bucket.uuid}
class="btn btn-xs btn-outline btn-info tooltip tooltip-bottom"
data-tip={
if bucket.enabled, do: gettext("Disable"), else: gettext("Enable")
}
>
<%= if bucket.enabled do %>
<.icon name="hero-eye-slash" class="w-4 h-4 hidden sm:inline" />
<% else %>
<.icon name="hero-eye" class="w-4 h-4 hidden sm:inline" />
<% end %>
<span class="sm:hidden whitespace-nowrap">
{if bucket.enabled, do: gettext("Disable"), else: gettext("Enable")}
</span>
</button>
<%!-- Delete Button --%>
<button
phx-click="delete_bucket"
phx-value-id={bucket.uuid}
data-confirm="Are you sure you want to delete this bucket? This will remove all location records."
class="btn btn-xs btn-outline btn-error tooltip tooltip-bottom"
data-tip={gettext("Delete bucket")}
>
<.icon name="hero-trash" class="w-4 h-4 hidden sm:inline" />
<span class="sm:hidden whitespace-nowrap">
{gettext("Delete bucket")}
</span>
</button>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<div class="stats stats-vertical lg:stats-horizontal shadow mt-6">
<div class="stat">
<div class="stat-title">Total Buckets</div>
<div class="stat-value text-2xl">{length(@buckets)}</div>
<div class="stat-desc">Configured storage locations</div>
</div>
<div class="stat">
<div class="stat-title">Active Buckets</div>
<div class="stat-value text-2xl text-success">
{Enum.count(@buckets, & &1.enabled)}
</div>
<div class="stat-desc">Currently in use</div>
</div>
<div class="stat">
<div class="stat-title">Default Bucket</div>
<div class="stat-value text-2xl">
<%= if @default_bucket_uuid do %>
<span class="text-success">Set</span>
<% else %>
<span class="text-warning">None</span>
<% end %>
</div>
<div class="stat-desc">Primary storage location</div>
</div>
</div>
<% end %>
</div>
</div>
<%!-- Storage Configuration --%>
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<h2 class="card-title text-2xl mb-4">
<.icon name="hero-cog-6-tooth" class="w-6 h-6 mr-2" /> Storage Configuration
</h2>
<.form
for={%{"form_redundancy" => @form_redundancy}}
phx-change="update_storage_form"
class="grid grid-cols-1 md:grid-cols-2 gap-6"
>
<%!-- Redundancy Settings --%>
<div class="form-control">
<div class="mb-2">
<span class="label-text font-semibold">Redundancy Copies</span>
<span class="block text-xs text-base-content/60">Number of copies to store</span>
</div>
<%= if @active_buckets_count == 0 do %>
<div class="alert alert-warning">
<.icon name="hero-exclamation-triangle" class="w-4 h-4" />
<span class="text-sm">
No active buckets available. Enable buckets to configure redundancy.
</span>
</div>
<% else %>
<%= if @form_redundancy > @max_redundancy do %>
<div class="alert alert-warning">
<.icon name="hero-exclamation-triangle" class="w-4 h-4" />
<span class="text-sm">
Current setting (#{@form_redundancy} copies) exceeds available buckets (#{@max_redundancy}).
Reduce redundancy or enable more buckets.
</span>
</div>
<% end %>
<input
type="number"
name="form_redundancy"
value={@form_redundancy}
min="1"
max={@max_redundancy}
class={"input input-bordered #{if @form_redundancy > @max_redundancy, do: "input-warning", else: ""}"}
/>
<p class="text-xs text-base-content/70 mt-1">
Enter number of copies (1-{@max_redundancy}).
<%= if @max_redundancy == 1 do %>
Only 1 active bucket available. Add more buckets to enable redundancy.
<% else %>
Maximum {@max_redundancy} copies based on {@active_buckets_count} active bucket(s).
<% end %>
</p>
<% end %>
</div>
<%!-- Auto-Variant Generation --%>
<div class="form-control">
<label class="flex items-start gap-3 cursor-pointer py-2">
<input
type="checkbox"
name="form_auto_generate_variants"
checked={@form_auto_generate_variants}
phx-click="toggle_form_variants"
value="true"
class="checkbox checkbox-primary mt-0.5 flex-shrink-0"
/>
<span>
<span class="font-semibold text-sm">Auto-Generate Variants</span>
<span class="block text-xs text-base-content/70 mt-0.5">
Automatically resize images and videos to configured dimensions
</span>
</span>
</label>
</div>
</.form>
<%!-- Apply Button --%>
<div class="form-control mt-4">
<button
type="button"
phx-click="apply_storage_settings"
disabled={
@form_redundancy == @redundancy_copies and
@form_auto_generate_variants == @auto_generate_variants
}
phx-disable-with="Applying..."
class="btn btn-primary"
>
<.icon name="hero-check" class="w-4 h-4 mr-2" /> Apply Changes
</button>
<span class="label-text-alt text-xs ml-2">
<%= if @form_redundancy == @redundancy_copies and @form_auto_generate_variants == @auto_generate_variants do %>
No changes to apply
<% else %>
Changes will be saved immediately
<% end %>
</span>
</div>
<%!-- Quick Actions --%>
<div class="divider">Quick Actions</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<.link
navigate={PhoenixKit.Utils.Routes.path("/admin/settings/media/dimensions")}
class="btn btn-outline"
>
<.icon name="hero-photo" class="w-4 h-4 mr-2" /> Manage Dimensions
</.link>
<button class="btn btn-outline" disabled>
<.icon name="hero-chart-bar" class="w-4 h-4 mr-2" /> Storage Stats
<span class="badge badge-xs ml-1">Soon</span>
</button>
<button class="btn btn-outline" disabled>
<.icon name="hero-arrow-path" class="w-4 h-4 mr-2" /> Sync Files
<span class="badge badge-xs ml-1">Soon</span>
</button>
</div>
</div>
</div>
<%!-- Coming Soon Features --%>
<div class="card bg-base-100 shadow-xl border-2 border-dashed border-base-300">
<div class="card-body">
<h2 class="card-title text-2xl mb-4">
<.icon name="hero-rocket-launch" class="w-6 h-6 mr-2" /> Advanced Features Coming Soon
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<%!-- Multi-Location Storage --%>
<div class="border border-base-300 rounded-lg p-4">
<div class="flex items-start gap-3">
<div class="text-3xl">🗂️</div>
<div>
<h3 class="font-bold mb-1">Multi-Location Storage</h3>
<p class="text-sm text-base-content/70">
Store files across 1-5 redundant locations with automatic failover
</p>
</div>
</div>
</div>
<%!-- S3 Integration --%>
<div class="border border-base-300 rounded-lg p-4">
<div class="flex items-start gap-3">
<div class="text-3xl">☁️</div>
<div>
<h3 class="font-bold mb-1">AWS S3 Integration</h3>
<p class="text-sm text-base-content/70">
Store files in Amazon S3 buckets for cloud redundancy
</p>
</div>
</div>
</div>
<%!-- Remote Servers --%>
<div class="border border-base-300 rounded-lg p-4">
<div class="flex items-start gap-3">
<div class="text-3xl">🖥️</div>
<div>
<h3 class="font-bold mb-1">Remote Servers</h3>
<p class="text-sm text-base-content/70">
Distribute files across multiple remote servers and datacenters
</p>
</div>
</div>
</div>
<%!-- Admin Dashboard --%>
<div class="border border-base-300 rounded-lg p-4">
<div class="flex items-start gap-3">
<div class="text-3xl">📊</div>
<div>
<h3 class="font-bold mb-1">Admin Dashboard</h3>
<p class="text-sm text-base-content/70">
Monitor storage health, locations, and file distribution
</p>
</div>
</div>
</div>
<%!-- File Tracking --%>
<div class="border border-base-300 rounded-lg p-4">
<div class="flex items-start gap-3">
<div class="text-3xl">📋</div>
<div>
<h3 class="font-bold mb-1">File Tracking</h3>
<p class="text-sm text-base-content/70">
PostgreSQL-based file registry with location tracking
</p>
</div>
</div>
</div>
<%!-- Automatic Failover --%>
<div class="border border-base-300 rounded-lg p-4">
<div class="flex items-start gap-3">
<div class="text-3xl">🔄</div>
<div>
<h3 class="font-bold mb-1">Automatic Failover</h3>
<p class="text-sm text-base-content/70">
Seamless file retrieval even when storage locations fail
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<%!-- Upload Test Section --%>
<%!-- Help Section --%>
<div class="alert mt-6">
<.icon name="hero-light-bulb" class="w-5 h-5" />
<div>
<h3 class="font-bold">Storage System Architecture</h3>
<p class="text-sm">
The storage system is designed for distributed file management with automatic redundancy.
Files are tracked in PostgreSQL with support for multiple storage locations (local, S3, remote servers).
When a file is requested, the system automatically tries each location until it successfully retrieves the file.
</p>
</div>
</div>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>
<%!-- Add CSS for drag and drop --%>
<style>
#upload-dropzone.dragover {
@apply border-primary bg-primary/5;
}
</style>
<script>
// Simple drag and drop styling
document.addEventListener('DOMContentLoaded', function() {
const dropzone = document.getElementById('upload-dropzone');
const fileInput = document.getElementById('file-upload');
if (dropzone && fileInput) {
dropzone.addEventListener('dragover', function(e) {
e.preventDefault();
dropzone.classList.add('dragover');
});
dropzone.addEventListener('dragleave', function(e) {
e.preventDefault();
dropzone.classList.remove('dragover');
});
dropzone.addEventListener('drop', function(e) {
e.preventDefault();
dropzone.classList.remove('dragover');
});
}
});
</script>