Packages
phoenix_kit
1.7.220
1.7.220
1.7.219
1.7.218
1.7.217
1.7.216
1.7.215
1.7.214
1.7.213
1.7.212
1.7.211
1.7.210
1.7.209
1.7.208
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
Current section
Files
lib/modules/storage/web/bucket_form.html.heex
<PhoenixKitWeb.Components.LayoutWrapper.app_layout
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title={@page_title}
page_section={gettext("Media")}
page_section_path={PhoenixKit.Utils.Routes.path("/admin/settings/media")}
page_subtitle={gettext("Configure storage provider settings")}
current_path={@current_path}
current_locale={@current_locale}
project_title={@project_title}
>
<div class="mx-auto px-4 py-8 max-w-3xl">
<.form for={@changeset} phx-change="validate" phx-submit="save" class="space-y-6">
<%!-- Basic Information --%>
<div class="card bg-base-100 shadow-sm">
<div class="card-body gap-4">
<h2 class="card-title text-lg">
<.icon name="hero-server" class="w-5 h-5" /> {gettext("Basic Information")}
</h2>
<fieldset class="fieldset">
<legend class="fieldset-legend font-semibold">{gettext("Bucket Name")} *</legend>
<.input
type="text"
name="bucket[name]"
value={Ecto.Changeset.get_field(@changeset, :name)}
class={input_class(@changeset, :name)}
placeholder={gettext("e.g., Production Media, Backup Storage")}
required
/>
<p class="text-xs text-base-content/50 mt-1">
{gettext("A friendly name to identify this storage location")}
</p>
</fieldset>
<%= if @mode == :new do %>
<fieldset class="fieldset">
<legend class="fieldset-legend font-semibold">
{gettext("Storage Provider")} *
</legend>
<.select
name="bucket[provider]"
required
value={Ecto.Changeset.get_field(@changeset, :provider)}
class={input_class(@changeset, :provider)}
prompt={gettext("Select provider...")}
options={[
{gettext("Local Filesystem"), "local"},
{"AWS S3", "s3"},
{"Backblaze B2", "b2"},
{"Cloudflare R2", "r2"},
{"Tigris", "tigris"}
]}
/>
</fieldset>
<% else %>
<input type="hidden" name="bucket[provider]" value={@current_provider} />
<fieldset class="fieldset">
<legend class="fieldset-legend font-semibold">{gettext("Storage Provider")}</legend>
<div class="flex items-center gap-2">
<span class="badge badge-lg badge-outline">
<%= case @current_provider do %>
<% "local" -> %>
{gettext("Local Filesystem")}
<% "s3" -> %>
AWS S3
<% "b2" -> %>
Backblaze B2
<% "r2" -> %>
Cloudflare R2
<% "tigris" -> %>
Tigris
<% _ -> %>
{String.upcase(@current_provider || "Unknown")}
<% end %>
</span>
<span class="text-xs text-base-content/40">{gettext("Cannot be changed")}</span>
</div>
</fieldset>
<% end %>
</div>
</div>
<%!-- Local Provider Settings --%>
<%= if @current_provider == "local" do %>
<div class="card bg-base-100 shadow-sm">
<div class="card-body gap-4">
<h2 class="card-title text-lg">
<.icon name="hero-folder" class="w-5 h-5" /> {gettext("Local Storage")}
</h2>
<fieldset class="fieldset">
<legend class="fieldset-legend font-semibold">{gettext("Storage Path")} *</legend>
<.input
type="text"
name="bucket[endpoint]"
value={Ecto.Changeset.get_field(@changeset, :endpoint)}
class={input_class(@changeset, :endpoint)}
placeholder="/path/to/storage"
required
/>
<p class="text-xs text-base-content/50 mt-1">
{gettext("Absolute path where files will be stored on disk")}
</p>
</fieldset>
</div>
</div>
<% end %>
<%!-- Cloud Provider Settings --%>
<%= if @current_provider in ["s3", "b2", "r2", "tigris"] do %>
<div class="card bg-base-100 shadow-sm">
<div class="p-8 space-y-4">
<h2 class="card-title text-lg">
<.icon name="hero-cloud" class="w-5 h-5" /> {gettext("Provider Configuration")}
</h2>
<%= if @current_provider == "s3" do %>
<.live_component
module={PhoenixKitWeb.Live.Components.SearchableSelect}
id="bucket-region-select"
name="bucket[region]"
value={Ecto.Changeset.get_field(@changeset, :region)}
label={"#{gettext("AWS Region")} *"}
placeholder="Search regions..."
required={true}
grouped_options={
Enum.map(AwsRegions.group_by_continent(), fn {continent, regions} ->
{continent, Enum.map(regions, fn r -> {"#{r.name} — #{r.code}", r.code} end)}
end)
}
/>
<% end %>
<%= if @current_provider == "b2" do %>
<.live_component
module={PhoenixKitWeb.Live.Components.SearchableSelect}
id="bucket-b2-region-select"
name="bucket[region]"
value={Ecto.Changeset.get_field(@changeset, :region)}
label={"#{gettext("Backblaze Region")} *"}
placeholder="Search Backblaze regions..."
required={true}
grouped_options={
Enum.map(BackblazeRegions.group_by_continent(), fn {continent, regions} ->
{continent, Enum.map(regions, fn r -> {"#{r.name} — #{r.code}", r.code} end)}
end)
}
/>
<% end %>
<%= if @current_provider == "tigris" do %>
<.live_component
module={PhoenixKitWeb.Live.Components.SearchableSelect}
id="bucket-tigris-region-select"
name="bucket[region]"
value={Ecto.Changeset.get_field(@changeset, :region)}
label={"#{gettext("Tigris Region")} *"}
placeholder="Search Tigris regions..."
required={true}
grouped_options={
Enum.map(Enum.sort(TigrisRegions.group_by_continent()), fn {continent, regions} ->
{continent, Enum.map(regions, fn r -> {"#{r.name} — #{r.code}", r.code} end)}
end)
}
/>
<% end %>
<fieldset class="fieldset">
<legend class="fieldset-legend font-semibold">
{case @current_provider do
"s3" -> gettext("AWS Bucket Name")
"b2" -> gettext("Backblaze Bucket Name")
"r2" -> gettext("Cloudflare Bucket Name")
"tigris" -> gettext("Tigris Bucket Name")
_ -> gettext("Bucket Name")
end} *
</legend>
<.input
type="text"
name="bucket[bucket_name]"
value={Ecto.Changeset.get_field(@changeset, :bucket_name)}
class={input_class(@changeset, :bucket_name)}
placeholder="e.g., my-app-files"
required={@current_provider in ["b2", "r2", "tigris"]}
/>
</fieldset>
<%= if @current_provider in ["b2", "r2", "tigris"] do %>
<fieldset class="fieldset">
<legend class="fieldset-legend font-semibold">
{case @current_provider do
"b2" -> gettext("Backblaze Endpoint")
"r2" -> gettext("Cloudflare Endpoint")
"tigris" -> gettext("Tigris Endpoint")
_ -> gettext("Endpoint")
end} *
</legend>
<.input
type="text"
name="bucket[endpoint]"
value={Ecto.Changeset.get_field(@changeset, :endpoint)}
class={input_class(@changeset, :endpoint)}
placeholder={
case @current_provider do
"b2" -> "e.g., s3.us-west-002.backblazeb2.com"
"r2" -> "e.g., {account_id}.r2.cloudflarestorage.com"
"tigris" -> "e.g., fly.storage.tigris.dev"
_ -> "e.g., endpoint.example.com"
end
}
required={@current_provider in ["b2", "r2", "tigris"]}
/>
</fieldset>
<% end %>
</div>
</div>
<%!-- Access Credentials --%>
<div class="card bg-base-100 shadow-sm">
<div class="card-body gap-4">
<h2 class="card-title text-lg">
<.icon name="hero-key" class="w-5 h-5" /> {gettext("Access Credentials")}
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<fieldset class="fieldset">
<legend class="fieldset-legend font-semibold">
{gettext("Access Key ID")} *
</legend>
<.input
type="text"
name="bucket[access_key_id]"
value={Ecto.Changeset.get_field(@changeset, :access_key_id)}
class={input_class(@changeset, :access_key_id)}
placeholder="AKIA..."
required={@current_provider in ["b2", "r2", "tigris"]}
/>
</fieldset>
<fieldset class="fieldset">
<legend class="fieldset-legend font-semibold">
{gettext("Secret Access Key")} *
</legend>
<.input
type="password"
name="bucket[secret_access_key]"
value={Ecto.Changeset.get_field(@changeset, :secret_access_key)}
class={input_class(@changeset, :secret_access_key)}
placeholder="•••••••••••••••••••••••••••••••••"
required={@current_provider in ["b2", "r2", "tigris"]}
/>
</fieldset>
</div>
<%!-- Test Connection --%>
<div class="pt-2">
<button
type="button"
phx-click="test_connection"
class={"btn btn-outline btn-sm #{if @connection_status == :success, do: "btn-success", else: ""}"}
disabled={@testing_connection}
>
<%= if @testing_connection do %>
<span class="loading loading-spinner loading-xs"></span>
{gettext("Testing...")}
<% else %>
<.icon name="hero-signal" class="w-4 h-4" />
{gettext("Test Connection")}
<% end %>
</button>
<%= if @connection_status == :success do %>
<span class="text-sm text-success ml-2">
<.icon name="hero-check-circle" class="w-4 h-4 inline" />
{gettext("Connection successful")}
</span>
<% end %>
<%= if @connection_status == :failed do %>
<div class="mt-2 text-sm text-error">
<.icon name="hero-x-circle" class="w-4 h-4 inline" />
{gettext("Connection failed")}
<%= if @connection_error do %>
: {@connection_error}
<% end %>
</div>
<% end %>
</div>
</div>
</div>
<% end %>
<%!-- Settings --%>
<div class="card bg-base-100 shadow-sm">
<div class="card-body gap-4">
<h2 class="card-title text-lg">
<.icon name="hero-cog-6-tooth" class="w-5 h-5" /> {gettext("Settings")}
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<%= if @current_provider in ["s3", "b2", "r2", "tigris"] do %>
<fieldset class="fieldset">
<legend class="fieldset-legend font-semibold">{gettext("Access Type")}</legend>
<.select
name="bucket[access_type]"
value={Ecto.Changeset.get_field(@changeset, :access_type)}
options={[
{gettext("Public (direct URL)"), "public"},
{gettext("Private (proxied)"), "private"}
]}
/>
</fieldset>
<% end %>
<fieldset class="fieldset">
<legend class="fieldset-legend font-semibold">{gettext("Priority")}</legend>
<.input
type="number"
name="bucket[priority]"
value={Ecto.Changeset.get_field(@changeset, :priority)}
class={input_class(@changeset, :priority)}
placeholder="0"
min="0"
/>
<p class="text-xs text-base-content/50 mt-1">
{gettext("0 = auto, 1+ = higher priority")}
</p>
</fieldset>
</div>
<div class="form-control">
<.checkbox
name="bucket[enabled]"
checked={Ecto.Changeset.get_field(@changeset, :enabled)}
>
<span class="font-semibold">{gettext("Enable bucket")}</span>
<:description>
{gettext("Only enabled buckets receive new uploads")}
</:description>
</.checkbox>
</div>
</div>
</div>
<%!-- Actions --%>
<div class="flex justify-end gap-3">
<.link
navigate={PhoenixKit.Utils.Routes.path("/admin/settings/media")}
class="btn btn-ghost"
>
{gettext("Cancel")}
</.link>
<button
type="submit"
class="btn btn-primary"
disabled={
!@changeset.valid? ||
(@current_provider in ["s3", "b2", "r2", "tigris"] && @connection_status != :success)
}
>
<.icon name="hero-check" class="w-4 h-4" /> {gettext("Save Bucket")}
</button>
</div>
</.form>
</div>
<%!-- Create Path Confirmation Modal --%>
<%= if @show_create_path_modal do %>
<div class="modal modal-open">
<div class="modal-box">
<h3 class="font-bold text-lg">{gettext("Create Storage Path")}</h3>
<p class="py-4">
{gettext("The storage path")}
<code class="badge badge-ghost">{@missing_path}</code> {gettext("does not exist.")}
</p>
<p class="mb-6">{gettext("Would you like to create it now?")}</p>
<div class="modal-action">
<button phx-click="cancel_create_path" class="btn btn-ghost">
{gettext("Cancel")}
</button>
<button
phx-click="confirm_create_path"
phx-value-path={@missing_path}
class="btn btn-primary"
>
<.icon name="hero-plus" class="w-4 h-4" /> {gettext("Create Path")}
</button>
</div>
</div>
</div>
<% end %>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>