diff --git a/CHANGELOG.md b/CHANGELOG.md
index 26066d59f..85c8e1c27 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,10 +11,11 @@
## \#2 unreleased
+* Changed: Format of the filenames in Storages. Also no folders used anymore. This has impact on Data Donation studies.
+* Changed: Assignment does not have a Storage association anymore. Projects can have one Storage that is shared between all the project items.
* Added: Storage project item
* Added: Support for German language in assignments
-
## \#1 2024-06-12
Initial version
\ No newline at end of file
diff --git a/core/frameworks/pixel/components/navigation.ex b/core/frameworks/pixel/components/navigation.ex
index 319d549b8..67a845491 100644
--- a/core/frameworks/pixel/components/navigation.ex
+++ b/core/frameworks/pixel/components/navigation.ex
@@ -45,20 +45,13 @@ defmodule Frameworks.Pixel.Navigation do
attr(:right_bar_buttons, :list, default: [])
attr(:more_buttons, :list, default: [])
- attr(:size, :atom, default: :wide)
attr(:hide_seperator, :boolean, default: true)
slot(:inner_block, required: true)
- def action_bar(%{size: size, right_bar_buttons: right_bar_buttons} = assigns) do
+ def action_bar(%{right_bar_buttons: right_bar_buttons} = assigns) do
assigns =
assign(assigns, %{
- has_right_bar_buttons: not Enum.empty?(right_bar_buttons),
- centralize:
- if size == :wide do
- Enum.empty?(right_bar_buttons)
- else
- false
- end
+ has_right_bar_buttons: not Enum.empty?(right_bar_buttons)
})
~H"""
@@ -70,16 +63,9 @@ defmodule Frameworks.Pixel.Navigation do
diff --git a/core/frameworks/pixel/components/table.ex b/core/frameworks/pixel/components/table.ex
new file mode 100644
index 000000000..3225e0ef2
--- /dev/null
+++ b/core/frameworks/pixel/components/table.ex
@@ -0,0 +1,122 @@
+defmodule Frameworks.Pixel.Table do
+ alias CoreWeb.UI.Timestamp
+ use CoreWeb, :html
+
+ attr(:layout, :list, required: true)
+ attr(:head_cells, :list, required: true)
+ attr(:rows, :list, required: true)
+ attr(:border, :boolean, default: true)
+
+ def table(assigns) do
+ ~H"""
+
+
<%= render_slot(@inner_block) %>
"""
end
attr(:color, :string, default: "text-grey1")
+ attr(:align, :string, default: "text-left")
slot(:inner_block, required: true)
def table_row(assigns) do
~H"""
-
+
<%= render_slot(@inner_block) %>
"""
diff --git a/core/lib/core/authorization.ex b/core/lib/core/authorization.ex
index 668cb1fc9..eea8da9ee 100644
--- a/core/lib/core/authorization.ex
+++ b/core/lib/core/authorization.ex
@@ -29,47 +29,47 @@ defmodule Core.Authorization do
grant_access(Systems.Lab.ToolModel, [:owner, :participant])
# Pages
- grant_access(Systems.Home.Page, [:visitor, :member, :creator])
- grant_access(Systems.Desktop.Page, [:creator])
- grant_access(Systems.Org.ContentPage, [:admin])
- grant_access(Systems.Admin.LoginPage, [:visitor, :member])
+ grant_access(CoreWeb.FakeQualtrics, [:member])
+ grant_access(Systems.Account.AwaitConfirmation, [:visitor])
+ grant_access(Systems.Account.ConfirmToken, [:visitor])
+ grant_access(Systems.Account.ResetPassword, [:visitor])
+ grant_access(Systems.Account.ResetPasswordToken, [:visitor])
+ grant_access(Systems.Account.SignupPage, [:visitor])
+ grant_access(Systems.Account.UserProfilePage, [:member])
+ grant_access(Systems.Account.UserSecuritySettings, [:member])
+ grant_access(Systems.Account.UserSettings, [:member])
+ grant_access(Systems.Account.UserSignin, [:visitor])
grant_access(Systems.Admin.ConfigPage, [:admin])
grant_access(Systems.Admin.ImportRewardsPage, [:admin])
- grant_access(Systems.Budget.FundingPage, [:admin, :creator])
- grant_access(Systems.Support.OverviewPage, [:admin])
- grant_access(Systems.Support.TicketPage, [:admin])
- grant_access(Systems.Support.HelpdeskPage, [:member])
- grant_access(Systems.Notification.OverviewPage, [:member])
- grant_access(Systems.NextAction.OverviewPage, [:member])
- grant_access(Systems.Advert.OverviewPage, [:creator])
+ grant_access(Systems.Admin.LoginPage, [:visitor, :member])
grant_access(Systems.Advert.ContentPage, [:owner])
- grant_access(Systems.Assignment.CrewPage, [:participant, :tester])
+ grant_access(Systems.Advert.OverviewPage, [:creator])
+ grant_access(Systems.Alliance.CallbackPage, [:owner])
grant_access(Systems.Assignment.ContentPage, [:owner])
+ grant_access(Systems.Assignment.CrewPage, [:participant, :tester])
grant_access(Systems.Assignment.LandingPage, [:participant])
- grant_access(Systems.Alliance.CallbackPage, [:owner])
+ grant_access(Systems.Budget.FundingPage, [:admin, :creator])
+ grant_access(Systems.Desktop.Page, [:creator])
+ grant_access(Systems.Feldspar.AppPage, [:visitor, :member])
+ grant_access(Systems.Graphite.LeaderboardContentPage, [:owner])
+ grant_access(Systems.Graphite.LeaderboardPage, [:owner, :participant, :tester])
+ grant_access(Systems.Home.Page, [:visitor, :member, :creator])
grant_access(Systems.Lab.PublicPage, [:member])
- grant_access(Systems.Promotion.LandingPage, [:visitor, :member])
+ grant_access(Systems.NextAction.OverviewPage, [:member])
+ grant_access(Systems.Notification.OverviewPage, [:member])
+ grant_access(Systems.Org.ContentPage, [:admin])
grant_access(Systems.Pool.DetailPage, [:creator])
grant_access(Systems.Pool.LandingPage, [:visitor, :member, :owner])
- grant_access(Systems.Pool.SubmissionPage, [:creator])
grant_access(Systems.Pool.ParticipantPage, [:creator])
- grant_access(Systems.Test.Page, [:visitor, :member])
- grant_access(Systems.Project.OverviewPage, [:admin, :creator])
+ grant_access(Systems.Pool.SubmissionPage, [:creator])
grant_access(Systems.Project.NodePage, [:creator, :owner])
- grant_access(Systems.Graphite.LeaderboardPage, [:owner, :participant, :tester])
- grant_access(Systems.Graphite.LeaderboardContentPage, [:owner])
- grant_access(Systems.Feldspar.AppPage, [:visitor, :member])
-
- grant_access(Systems.Account.UserSignin, [:visitor])
- grant_access(Systems.Account.SignupPage, [:visitor])
- grant_access(Systems.Account.ResetPassword, [:visitor])
- grant_access(Systems.Account.ResetPasswordToken, [:visitor])
- grant_access(Systems.Account.AwaitConfirmation, [:visitor])
- grant_access(Systems.Account.ConfirmToken, [:visitor])
- grant_access(Systems.Account.UserProfilePage, [:member])
- grant_access(Systems.Account.UserSettings, [:member])
- grant_access(Systems.Account.UserSecuritySettings, [:member])
- grant_access(CoreWeb.FakeQualtrics, [:member])
+ grant_access(Systems.Project.OverviewPage, [:admin, :creator])
+ grant_access(Systems.Promotion.LandingPage, [:visitor, :member])
+ grant_access(Systems.Storage.EndpointContentPage, [:owner])
+ grant_access(Systems.Support.HelpdeskPage, [:member])
+ grant_access(Systems.Support.OverviewPage, [:admin])
+ grant_access(Systems.Support.TicketPage, [:admin])
+ grant_access(Systems.Test.Page, [:visitor, :member])
grant_actions(CoreWeb.FakeAllianceController, %{
index: [:visitor, :member]
@@ -372,6 +372,7 @@ defmodule Core.Authorization do
Systems.Project.NodeModel,
Systems.Workflow.Model,
Systems.Assignment.Model,
+ Systems.Storage.EndpointModel,
Systems.Crew.Model,
Systems.Graphite.ToolModel,
Systems.Graphite.LeaderboardModel
diff --git a/core/lib/core_web/ui/timestamp.ex b/core/lib/core_web/ui/timestamp.ex
index 260206b57..39caebb14 100644
--- a/core/lib/core_web/ui/timestamp.ex
+++ b/core/lib/core_web/ui/timestamp.ex
@@ -169,6 +169,42 @@ defmodule CoreWeb.UI.Timestamp do
Timex.format!(datetime, "%Y-%m-%d", :strftime)
end
+ def stamp(%DateTime{} = datetime) do
+ weekday = Timex.format!(datetime, "%A", :strftime)
+ month = Timex.format!(datetime, "%B", :strftime)
+ day_of_month = Timex.format!(datetime, "%e", :strftime)
+ time = Timex.format!(datetime, "%H:%M", :strftime)
+
+ dgettext("eyra-ui", "timestamp.datetime",
+ weekday: weekday,
+ day_of_month: day_of_month,
+ month: month,
+ time: time
+ )
+ end
+
+ @spec humanize_time(
+ {{integer(), pos_integer(), pos_integer()},
+ {non_neg_integer(), non_neg_integer(), non_neg_integer()}
+ | {non_neg_integer(), non_neg_integer(), non_neg_integer(),
+ non_neg_integer() | {non_neg_integer(), non_neg_integer()}}}
+ | {integer(), pos_integer(), pos_integer()}
+ | %{
+ :__struct__ => Date | DateTime | NaiveDateTime | Time,
+ :calendar => atom(),
+ optional(:day) => pos_integer(),
+ optional(:hour) => non_neg_integer(),
+ optional(:microsecond) => {non_neg_integer(), non_neg_integer()},
+ optional(:minute) => non_neg_integer(),
+ optional(:month) => pos_integer(),
+ optional(:second) => non_neg_integer(),
+ optional(:std_offset) => integer(),
+ optional(:time_zone) => binary(),
+ optional(:utc_offset) => integer(),
+ optional(:year) => integer(),
+ optional(:zone_abbr) => binary()
+ }
+ ) :: binary()
def humanize_time(timestamp) do
Timex.format!(timestamp, "%H:%M", :strftime)
end
diff --git a/core/priv/gettext/de/LC_MESSAGES/eyra-storage.po b/core/priv/gettext/de/LC_MESSAGES/eyra-storage.po
index 1926f2b8c..7306a48f8 100644
--- a/core/priv/gettext/de/LC_MESSAGES/eyra-storage.po
+++ b/core/priv/gettext/de/LC_MESSAGES/eyra-storage.po
@@ -90,3 +90,39 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "goto.storage.body"
msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "tabbar.item.data"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "tabbar.item.data.forward"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "tabbar.item.settings"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "tabbar.item.settings.forward"
+msgstr ""
+
+#, elixir-autogen, elixir-format, fuzzy
+msgid "tabbar.item.monitor"
+msgstr ""
+
+#, elixir-autogen, elixir-format, fuzzy
+msgid "tabbar.item.monitor.forward"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "table.date.label"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "table.file.label"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "table.size.label"
+msgstr ""
diff --git a/core/priv/gettext/en/LC_MESSAGES/eyra-storage.po b/core/priv/gettext/en/LC_MESSAGES/eyra-storage.po
index fef54c8c2..52976acdb 100644
--- a/core/priv/gettext/en/LC_MESSAGES/eyra-storage.po
+++ b/core/priv/gettext/en/LC_MESSAGES/eyra-storage.po
@@ -90,3 +90,39 @@ msgstr "Select the storage brand you want to use and click on the create button.
#, elixir-autogen, elixir-format
msgid "goto.storage.body"
msgstr "Manage the connected storage by following the link below."
+
+#, elixir-autogen, elixir-format
+msgid "tabbar.item.data"
+msgstr "Data"
+
+#, elixir-autogen, elixir-format
+msgid "tabbar.item.data.forward"
+msgstr "Go to Data"
+
+#, elixir-autogen, elixir-format
+msgid "tabbar.item.settings"
+msgstr "Settings"
+
+#, elixir-autogen, elixir-format
+msgid "tabbar.item.settings.forward"
+msgstr "Go to Settings"
+
+#, elixir-autogen, elixir-format, fuzzy
+msgid "tabbar.item.monitor"
+msgstr "Monitor"
+
+#, elixir-autogen, elixir-format, fuzzy
+msgid "tabbar.item.monitor.forward"
+msgstr "Go to Monitor"
+
+#, elixir-autogen, elixir-format
+msgid "table.date.label"
+msgstr "Date"
+
+#, elixir-autogen, elixir-format
+msgid "table.file.label"
+msgstr "File"
+
+#, elixir-autogen, elixir-format
+msgid "table.size.label"
+msgstr "Size"
diff --git a/core/priv/gettext/eyra-storage.pot b/core/priv/gettext/eyra-storage.pot
index 18ba79d8f..159762c6c 100644
--- a/core/priv/gettext/eyra-storage.pot
+++ b/core/priv/gettext/eyra-storage.pot
@@ -90,3 +90,39 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "goto.storage.body"
msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "tabbar.item.data"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "tabbar.item.data.forward"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "tabbar.item.settings"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "tabbar.item.settings.forward"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "tabbar.item.monitor"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "tabbar.item.monitor.forward"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "table.date.label"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "table.file.label"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "table.size.label"
+msgstr ""
diff --git a/core/priv/gettext/nl/LC_MESSAGES/eyra-storage.po b/core/priv/gettext/nl/LC_MESSAGES/eyra-storage.po
index 85c679a8e..bfb9408ce 100644
--- a/core/priv/gettext/nl/LC_MESSAGES/eyra-storage.po
+++ b/core/priv/gettext/nl/LC_MESSAGES/eyra-storage.po
@@ -90,3 +90,39 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "goto.storage.body"
msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "tabbar.item.data"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "tabbar.item.data.forward"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "tabbar.item.settings"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "tabbar.item.settings.forward"
+msgstr ""
+
+#, elixir-autogen, elixir-format, fuzzy
+msgid "tabbar.item.monitor"
+msgstr ""
+
+#, elixir-autogen, elixir-format, fuzzy
+msgid "tabbar.item.monitor.forward"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "table.date.label"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "table.file.label"
+msgstr ""
+
+#, elixir-autogen, elixir-format
+msgid "table.size.label"
+msgstr ""
diff --git a/core/systems/assignment/crew_page.ex b/core/systems/assignment/crew_page.ex
index 518bd5acd..b5bcde495 100644
--- a/core/systems/assignment/crew_page.ex
+++ b/core/systems/assignment/crew_page.ex
@@ -5,13 +5,13 @@ defmodule Systems.Assignment.CrewPage do
require Logger
- alias CoreWeb.UI.Timestamp
alias Core.ImageHelpers
- alias Frameworks.Signal
alias Frameworks.Pixel.Hero
alias Frameworks.Pixel.ModalView
+ alias Frameworks.Signal
alias Systems.Assignment
+ alias Systems.Project
alias Systems.Storage
@impl true
@@ -130,7 +130,7 @@ defmodule Systems.Assignment.CrewPage do
socket
) do
Assignment.Public.decline_member(model, user)
- socket = store(socket, nil, "onboarding", "{\"status\":\"consent declined\"}")
+ socket = store(socket, "", "", "onboarding", "{\"status\":\"consent declined\"}")
socket =
if embedded? do
@@ -146,8 +146,8 @@ defmodule Systems.Assignment.CrewPage do
end
@impl true
- def handle_event("store", %{key: key, group: group, data: data}, socket) do
- {:noreply, socket |> store(key, group, data)}
+ def handle_event("store", %{task: task, key: key, group: group, data: data}, socket) do
+ {:noreply, socket |> store(task, key, group, data)}
end
@impl true
@@ -176,24 +176,34 @@ defmodule Systems.Assignment.CrewPage do
remote_ip: remote_ip
}
} = socket,
+ task,
key,
group,
data
) do
+ participant = Map.get(panel_info, :participant, "")
+
meta_data = %{
remote_ip: remote_ip,
- timestamp: Timestamp.now() |> DateTime.to_unix(),
- key: key,
- group: group
+ panel_info: panel_info,
+ identifier: [
+ [:assignment, assignment.id],
+ [:task, task],
+ [:participant, participant],
+ [:source, group],
+ [:key, key]
+ ]
}
- if storage_info = Storage.Private.storage_info(assignment) do
- Storage.Public.store(storage_info, panel_info, data, meta_data)
+ with storage_endpoint <- Project.Public.get_storage_endpoint_by(assignment),
+ storage_info <- Storage.Private.storage_info(storage_endpoint, assignment) do
+ Storage.Public.store(storage_info, data, meta_data)
socket
else
- message = "Please setup connection to a data storage"
- Logger.error(message)
- socket |> put_flash(:error, message)
+ _ ->
+ message = "Please setup connection to a data storage"
+ Logger.error(message)
+ socket |> put_flash(:error, message)
end
end
diff --git a/core/systems/assignment/crew_work_view.ex b/core/systems/assignment/crew_work_view.ex
index ba5a7edc7..5ac8dd8b1 100644
--- a/core/systems/assignment/crew_work_view.ex
+++ b/core/systems/assignment/crew_work_view.ex
@@ -424,13 +424,16 @@ defmodule Systems.Assignment.CrewWorkView do
end
end
- defp handle_feldspar_event(%{assigns: %{selected_item: {%{group: group}, _}}} = socket, %{
- "__type__" => "CommandSystemDonate",
- "key" => key,
- "json_string" => json_string
- }) do
+ defp handle_feldspar_event(
+ %{assigns: %{selected_item: {%{id: task, group: group}, _}}} = socket,
+ %{
+ "__type__" => "CommandSystemDonate",
+ "key" => key,
+ "json_string" => json_string
+ }
+ ) do
socket
- |> send_event(:parent, "store", %{key: key, group: group, data: json_string})
+ |> send_event(:parent, "store", %{task: task, key: key, group: group, data: json_string})
|> Frameworks.Pixel.Flash.put_info("Donated")
end
diff --git a/core/systems/graphite/leaderboard_score_html.ex b/core/systems/graphite/leaderboard_score_html.ex
index d78a1c6ca..dae4946b0 100644
--- a/core/systems/graphite/leaderboard_score_html.ex
+++ b/core/systems/graphite/leaderboard_score_html.ex
@@ -1,9 +1,11 @@
defmodule Systems.Graphite.LeaderboardScoreHTML do
use CoreWeb, :html
+ import Frameworks.Pixel.Table
+
attr(:scores, :list, required: true)
- def table(assigns) do
+ def html(%{scores: scores} = assigns) do
head_cells = [
dgettext("eyra-graphite", "leaderboard.position.label"),
dgettext("eyra-graphite", "leaderboard.team.label"),
@@ -20,107 +22,16 @@ defmodule Systems.Graphite.LeaderboardScoreHTML do
%{type: :text, width: "w-24", align: "text-right"}
]
- assigns = assign(assigns, %{head_cells: head_cells, layout: layout})
-
- ~H"""
-
-
-
- <.row top={true} cells={@head_cells} layout={@layout}/>
-
- <%= for {%{team: team, description: description, url: url, value: value}, index} <- Enum.with_index(@scores) do %>
- <.row
- bottom={index == Enum.count(@scores)-1}
- cells={[
- "#{index+1}.",
- team,
- description,
- url,
- :erlang.float_to_binary(value, [decimals: 2])
- ]}
- layout={@layout}
- />
- <% end %>
-
-
- """
- end
+ rows =
+ scores
+ |> Enum.map(fn %{team: team, description: description, url: url, value: value} ->
+ [team, description, url, value]
+ end)
- attr(:cells, :list, required: true)
- attr(:layout, :list, required: true)
- attr(:top, :boolean, default: false)
- attr(:bottom, :boolean, default: false)
+ assigns = assign(assigns, head_cells: head_cells, layout: layout, rows: rows)
- def row(assigns) do
~H"""
-
- <%= for {cell, index} <- Enum.with_index(@cells) do %>
- <.cell
- content={cell}
- left={index == 0}
- right={index == Enum.count(@cells)-1}
- layout={Enum.at(@layout, index)}
- top={@top}
- bottom={@bottom}
- />
- <% end %>
-
+ <.table layout={@layout} head_cells={@head_cells} rows={@rows} />
"""
end
-
- defp cell_padding(%{top: true}), do: "pt-2"
- defp cell_padding(_), do: "p-0"
-
- attr(:content, :string, required: true)
- attr(:layout, :map, required: true)
- attr(:top, :boolean, default: false)
- attr(:bottom, :boolean, default: false)
- attr(:left, :boolean, default: false)
- attr(:right, :boolean, default: false)
-
- def cell(%{layout: layout, content: content} = assigns) do
- layout =
- if layout.type == :href and not valid_url?(content) do
- %{layout | type: :string}
- else
- layout
- end
-
- padding = cell_padding(assigns)
- assigns = assign(assigns, %{padding: padding, layout: layout})
-
- ~H"""
-
-
- <%= if not @top do %>
-
- <% end %>
-
-
-
-
- <%= if @top do %>
-
<%= @content %>
- <% else %>
-
- <%= if @layout.type == :href do %>
- Link
- <% else %>
- <%= @content %>
- <% end %>
-
- <% end %>
-
-
-
-
-
-
- """
- end
-
- defp valid_url?(string) do
- uri = URI.parse(string)
- uri.scheme != nil && uri.host =~ "."
- end
end
diff --git a/core/systems/graphite/leaderboard_table_view.ex b/core/systems/graphite/leaderboard_table_view.ex
index f0cc4d0bd..13a47e57b 100644
--- a/core/systems/graphite/leaderboard_table_view.ex
+++ b/core/systems/graphite/leaderboard_table_view.ex
@@ -86,7 +86,7 @@ defmodule Systems.Graphite.LeaderboardTableView do
<.spacing value="M" />
<%= if @active_metric do %>
- <.table scores={@scores} />
+ <.html scores={@scores} />
<% end %>
"""
diff --git a/core/systems/project/_public.ex b/core/systems/project/_public.ex
index ad24af234..223733840 100644
--- a/core/systems/project/_public.ex
+++ b/core/systems/project/_public.ex
@@ -50,6 +50,31 @@ defmodule Systems.Project.Public do
|> Repo.one!()
end
+ def get_storage_endpoint_by(%Assignment.Model{} = assignment) do
+ assignment
+ |> Project.Public.get_item_by()
+ |> get_storage_endpoint_by()
+ end
+
+ def get_storage_endpoint_by(%Project.ItemModel{} = project_item) do
+ project_item
+ |> Project.Public.get_node_by_item!([:auth_node])
+ |> get_storage_endpoint_by()
+ end
+
+ def get_storage_endpoint_by(%Project.NodeModel{} = project_node) do
+ storage_endpoint_item =
+ project_node
+ |> Project.Public.list_items(:storage_endpoint, Project.ItemModel.preload_graph(:down))
+ |> List.first()
+
+ if storage_endpoint_item do
+ Map.get(storage_endpoint_item, :storage_endpoint)
+ else
+ nil
+ end
+ end
+
def get_item_by(assignment, preload \\ [])
def get_item_by(%Assignment.Model{id: assignment_id}, preload) do
diff --git a/core/systems/storage/_presenter.ex b/core/systems/storage/_presenter.ex
new file mode 100644
index 000000000..d214afdd7
--- /dev/null
+++ b/core/systems/storage/_presenter.ex
@@ -0,0 +1,10 @@
+defmodule Systems.Storage.Presenter do
+ @behaviour Frameworks.Concept.Presenter
+
+ alias Systems.Storage
+
+ @impl true
+ def view_model(Storage.EndpointContentPage, node, assigns) do
+ Storage.EndpointContentPageBuilder.view_model(node, assigns)
+ end
+end
diff --git a/core/systems/storage/_private.ex b/core/systems/storage/_private.ex
index 4981dc7ed..91ffa6b93 100644
--- a/core/systems/storage/_private.ex
+++ b/core/systems/storage/_private.ex
@@ -1,7 +1,5 @@
defmodule Systems.Storage.Private do
- alias Systems.{
- Storage
- }
+ alias Systems.Storage
@centerdata_callback_url "https://quest.centerdata.nl/eyra/dd.php"
@@ -18,24 +16,46 @@ defmodule Systems.Storage.Private do
def build_special(:aws), do: %Storage.AWS.EndpointModel{}
def build_special(:azure), do: %Storage.Azure.EndpointModel{}
- def backend_info(%Storage.BuiltIn.EndpointModel{}), do: {:builtin, Storage.BuiltIn.Backend}
- def backend_info(%Storage.Yoda.EndpointModel{}), do: {:yoda, Storage.Yoda.Backend}
- def backend_info(%Storage.AWS.EndpointModel{}), do: {:aws, Storage.AWS.Backend}
- def backend_info(%Storage.Azure.EndpointModel{}), do: {:azure, Storage.Azure.Backend}
+ def special_info(%Storage.EndpointModel{} = endpoint) do
+ endpoint
+ |> Storage.EndpointModel.special()
+ |> special_info()
+ end
+
+ def special_info(%Storage.BuiltIn.EndpointModel{}), do: {:builtin, Storage.BuiltIn.Backend}
+ def special_info(%Storage.Yoda.EndpointModel{}), do: {:yoda, Storage.Yoda.Backend}
+ def special_info(%Storage.AWS.EndpointModel{}), do: {:aws, Storage.AWS.Backend}
+ def special_info(%Storage.Azure.EndpointModel{}), do: {:azure, Storage.Azure.Backend}
- def storage_info(%{storage_endpoint: %{} = storage_endpoint, external_panel: external_panel}) do
+ @spec storage_info(any()) ::
+ nil
+ | %{
+ backend:
+ Systems.Storage.AWS.Backend
+ | Systems.Storage.Azure.Backend
+ | Systems.Storage.BuiltIn.Backend
+ | Systems.Storage.Centerdata.Backend
+ | Systems.Storage.Yoda.Backend,
+ endpoint: %{
+ :__struct__ =>
+ Systems.Storage.AWS.EndpointModel
+ | Systems.Storage.Azure.EndpointModel
+ | Systems.Storage.BuiltIn.EndpointModel
+ | Systems.Storage.Centerdata.EndpointModel
+ | Systems.Storage.Yoda.EndpointModel,
+ optional(any()) => any()
+ },
+ key: :aws | :azure | :builtin | :centerdata | :yoda
+ }
+ def storage_info(storage_endpoint, %{external_panel: external_panel}) do
if endpoint = Storage.EndpointModel.special(storage_endpoint) do
- {key, backend} = backend_info(endpoint)
+ {key, backend} = special_info(endpoint)
%{key: key, backend: backend, endpoint: endpoint}
else
storage_info(external_panel)
end
end
- def storage_info(%{external_panel: external_panel}) do
- storage_info(external_panel)
- end
-
def storage_info(:liss) do
endpoint = %Storage.Centerdata.EndpointModel{url: @centerdata_callback_url}
backend = Storage.Centerdata.Backend
diff --git a/core/systems/storage/_public.ex b/core/systems/storage/_public.ex
index 6aa52bfa7..2ba5d2638 100644
--- a/core/systems/storage/_public.ex
+++ b/core/systems/storage/_public.ex
@@ -1,6 +1,8 @@
defmodule Systems.Storage.Public do
require Logger
+ alias Ecto.Changeset
+ alias Core.Authorization
alias Core.Repo
alias Systems.Rate
alias Systems.Storage
@@ -14,7 +16,8 @@ defmodule Systems.Storage.Public do
special_changeset = prepare_endpoint_special(special_type, attrs)
%Storage.EndpointModel{}
- |> Storage.EndpointModel.reset_special(special_type, special_changeset)
+ |> Storage.EndpointModel.change_special(special_type, special_changeset)
+ |> Changeset.put_assoc(:auth_node, Authorization.prepare_node())
end
defp prepare_endpoint_special(:builtin, attrs) do
@@ -39,9 +42,8 @@ defmodule Systems.Storage.Public do
def store(
%{key: key, backend: backend, endpoint: endpoint},
- %{embedded?: embedded?} = panel_info,
data,
- %{remote_ip: remote_ip} = meta_data
+ %{remote_ip: remote_ip, panel_info: %{embedded?: embedded?}} = meta_data
) do
packet_size = String.length(data)
@@ -51,12 +53,11 @@ defmodule Systems.Storage.Public do
if embedded? do
# submit data in current process
Logger.warn("[Storage.Public] deliver directly")
- Storage.Delivery.deliver(backend, endpoint, panel_info, data, meta_data)
+ Storage.Delivery.deliver(backend, endpoint, data, meta_data)
else
%{
backend: backend,
endpoint: endpoint,
- panel_info: panel_info,
data: data,
meta_data: meta_data
}
@@ -65,8 +66,14 @@ defmodule Systems.Storage.Public do
end
end
- def file_count(%Storage.EndpointModel{}) do
- 0
+ def list_files(endpoint) do
+ special = Storage.EndpointModel.special(endpoint)
+ {_, backend} = Storage.Private.special_info(special)
+ apply(backend, :list_files, [special])
+ end
+
+ def file_count(endpoint) do
+ list_files(endpoint) |> Enum.count()
end
end
diff --git a/core/systems/storage/_routes.ex b/core/systems/storage/_routes.ex
index 16fc52bef..822124686 100644
--- a/core/systems/storage/_routes.ex
+++ b/core/systems/storage/_routes.ex
@@ -3,7 +3,7 @@ defmodule Systems.Storage.Routes do
quote do
scope "/", Systems.Storage do
pipe_through([:browser, :require_authenticated_user])
- live("/storage/:id/content", ContentPage)
+ live("/storage/:id/content", EndpointContentPage)
end
end
end
diff --git a/core/systems/storage/aws/backend.ex b/core/systems/storage/aws/backend.ex
index 60bb76c00..25fa40e14 100644
--- a/core/systems/storage/aws/backend.ex
+++ b/core/systems/storage/aws/backend.ex
@@ -1,24 +1,29 @@
defmodule Systems.Storage.AWS.Backend do
@behaviour Systems.Storage.Backend
+ require Logger
alias ExAws.S3
+ @impl true
def store(
%{"s3_bucket_name" => bucket} = _endpoint,
- panel_info,
data,
meta_data
) do
[data]
- |> S3.upload(bucket, path(panel_info, meta_data))
+ |> S3.upload(bucket, filename(meta_data))
|> ExAws.request()
end
- defp path(%{"participant" => participant}, %{"key" => key, "timestamp" => timestamp}) do
- "#{participant}/#{key}/#{timestamp}.json"
+ @impl true
+ def list_files(_endpoint) do
+ Logger.error("Not yet implemented: files/4")
+ {:error, :not_implemented}
end
- defp path(%{"participant" => participant}, %{"key" => key}) do
- "#{key}/#{participant}.json"
+ defp filename(%{"identifier" => identifier}) do
+ identifier
+ |> Enum.map_join("_", fn [key, value] -> "#{key}-#{value}" end)
+ |> then(&"#{&1}.json")
end
end
diff --git a/core/systems/storage/azure/backend.ex b/core/systems/storage/azure/backend.ex
index 61b8f5c63..3b7facb56 100644
--- a/core/systems/storage/azure/backend.ex
+++ b/core/systems/storage/azure/backend.ex
@@ -5,18 +5,17 @@ defmodule Systems.Storage.Azure.Backend do
def store(
endpoint,
- panel_info,
data,
meta_data
) do
- path = path(panel_info, meta_data)
+ filename = filename(meta_data)
headers = [
{"Content-Type", "text/plain"},
{"x-ms-blob-type", "BlockBlob"}
]
- case url(endpoint, path) do
+ case url(endpoint, filename) do
{:ok, url} ->
HTTPoison.put(url, data, headers)
|> case do
@@ -36,12 +35,15 @@ defmodule Systems.Storage.Azure.Backend do
end
end
- defp path(%{"participant" => participant}, %{"key" => key, "timestamp" => _}) do
- "#{participant}/#{key}.json"
+ def list_files(_endpoint) do
+ Logger.error("Not yet implemented: files/4")
+ {:error, :not_implemented}
end
- defp path(%{"participant" => participant}, %{"key" => key}) do
- "#{key}/#{participant}.json"
+ defp filename(%{"identifier" => identifier}) do
+ identifier
+ |> Enum.map_join("_", fn [key, value] -> "#{key}-#{value}" end)
+ |> then(&"#{&1}.json")
end
defp url(
diff --git a/core/systems/storage/backend.ex b/core/systems/storage/backend.ex
index 24c3a8beb..aaaa8284a 100644
--- a/core/systems/storage/backend.ex
+++ b/core/systems/storage/backend.ex
@@ -1,8 +1,9 @@
defmodule Systems.Storage.Backend do
@callback store(
endpoint :: map(),
- panel_info :: map(),
data :: binary(),
meta_data :: map()
) :: any()
+
+ @callback list_files(endpoint :: map()) :: {:ok, list()} | {:error, atom()}
end
diff --git a/core/systems/storage/built_in/backend.ex b/core/systems/storage/built_in/backend.ex
index b6a80a0ae..4f2b540eb 100644
--- a/core/systems/storage/built_in/backend.ex
+++ b/core/systems/storage/built_in/backend.ex
@@ -1,35 +1,30 @@
defmodule Systems.Storage.BuiltIn.Backend do
@behaviour Systems.Storage.Backend
- alias CoreWeb.UI.Timestamp
+ require Logger
+
alias Systems.Storage.BuiltIn
- def store(%{"key" => folder}, panel_info, data, meta_data) do
- identifier = identifier(panel_info, meta_data)
- special().store(folder, identifier, data)
+ @impl true
+ def store(%{"key" => folder}, data, meta_data) do
+ filename = filename(meta_data)
+ special().store(folder, filename, data)
end
- def store(_, _, _, _) do
+ @impl true
+ def store(_, _, _) do
{:error, :endpoint_key_missing}
end
- defp identifier(%{"participant" => participant}, %{"key" => meta_key, "group" => group})
- when not is_nil(group) do
- ["participant=#{participant}", "source=#{group}", meta_key]
- end
-
- defp identifier(%{"participant" => participant}, %{"key" => meta_key}) do
- ["participant=#{participant}", meta_key]
- end
-
- defp identifier(%{"participant" => participant}, _) do
- timestamp = Timestamp.now() |> DateTime.to_unix()
- ["participant=#{participant}", "#{timestamp}"]
+ @impl true
+ def list_files(%{key: folder}) do
+ special().list_files(folder)
end
- defp identifier(_, _) do
- timestamp = Timestamp.now() |> DateTime.to_unix()
- ["participant=?", "#{timestamp}"]
+ defp filename(%{"identifier" => identifier}) do
+ identifier
+ |> Enum.map_join("_", fn [key, value] -> "#{key}=#{value}" end)
+ |> then(&"#{&1}.json")
end
defp settings do
diff --git a/core/systems/storage/built_in/local_fs.ex b/core/systems/storage/built_in/local_fs.ex
index d3f5c830f..31590c72a 100644
--- a/core/systems/storage/built_in/local_fs.ex
+++ b/core/systems/storage/built_in/local_fs.ex
@@ -11,6 +11,12 @@ defmodule Systems.Storage.BuiltIn.LocalFS do
File.write!(file_path, data)
end
+ @impl true
+ def list_files(folder) do
+ folder_path = get_full_path(folder)
+ File.ls!(folder_path)
+ end
+
defp get_full_path(folder) do
Path.join(get_root_path(), folder)
end
diff --git a/core/systems/storage/built_in/s3.ex b/core/systems/storage/built_in/s3.ex
index 8c314d44e..a53b0e80b 100644
--- a/core/systems/storage/built_in/s3.ex
+++ b/core/systems/storage/built_in/s3.ex
@@ -3,8 +3,7 @@ defmodule Systems.Storage.BuiltIn.S3 do
alias ExAws.S3
@impl true
- def store(folder, identifier, data) do
- filename = Enum.join(identifier, "_") <> ".json"
+ def store(folder, filename, data) do
filepath = Path.join(folder, filename)
object_key = object_key(filepath)
content_type = content_type(object_key)
@@ -14,6 +13,29 @@ defmodule Systems.Storage.BuiltIn.S3 do
|> backend().request!()
end
+ @impl true
+ def list_files(folder) do
+ bucket = Access.fetch!(settings(), :bucket)
+ prefix = object_key(folder) <> "/"
+
+ %{body: %{contents: contents}} =
+ S3.list_objects(bucket, prefix: prefix)
+ |> backend().request!()
+
+ contents
+ |> Enum.map(fn %{key: key, size: size, last_modified: last_modified} ->
+ path = String.replace_prefix(key, prefix, "")
+
+ timestamp =
+ case Timex.parse(last_modified, "{ISO:Extended:Z}") do
+ {:ok, result} -> result
+ _ -> nil
+ end
+
+ %{path: path, size: size, timestamp: timestamp}
+ end)
+ end
+
defp object_key(filepath) do
if prefix = Access.get(settings(), :prefix, nil) do
Path.join(prefix, filepath)
diff --git a/core/systems/storage/built_in/special.ex b/core/systems/storage/built_in/special.ex
index 45187db42..289258523 100644
--- a/core/systems/storage/built_in/special.ex
+++ b/core/systems/storage/built_in/special.ex
@@ -1,7 +1,9 @@
defmodule Systems.Storage.BuiltIn.Special do
@callback store(
folder :: binary(),
- identifier :: list(binary()),
+ identifier :: list(tuple()),
data :: binary()
) :: any()
+
+ @callback list_files(folder :: binary()) :: list()
end
diff --git a/core/systems/storage/centerdata/backend.ex b/core/systems/storage/centerdata/backend.ex
index 616d2da69..003a9c597 100644
--- a/core/systems/storage/centerdata/backend.ex
+++ b/core/systems/storage/centerdata/backend.ex
@@ -5,17 +5,18 @@ defmodule Systems.Storage.Centerdata.Backend do
def store(
%{url: url} = _endpoint,
+ data,
%{
- query_string: %{
- "quest" => quest,
- "varname1" => varname1,
- "respondent" => respondent,
- "token" => token,
- "page" => page
+ panel_info: %{
+ query_string: %{
+ "quest" => quest,
+ "varname1" => varname1,
+ "respondent" => respondent,
+ "token" => token,
+ "page" => page
+ }
}
- } = _panel_info,
- data,
- _meta_data
+ }
) do
Logger.warn("Centerdata store: respondent=#{respondent}")
@@ -40,4 +41,9 @@ defmodule Systems.Storage.Centerdata.Backend do
send(self(), %{storage_event: %{panel: :centerdata, form: form}})
end
+
+ def list_files(_endpoint) do
+ Logger.error("Not yet implemented: files/4")
+ {:error, :not_implemented}
+ end
end
diff --git a/core/systems/storage/delivery.ex b/core/systems/storage/delivery.ex
index ae222a164..f440b4620 100644
--- a/core/systems/storage/delivery.ex
+++ b/core/systems/storage/delivery.ex
@@ -25,11 +25,11 @@ defmodule Systems.Storage.Delivery do
end
end
- def deliver(backend, endpoint, panel_info, data, meta_data) do
+ def deliver(backend, endpoint, data, meta_data) do
Logger.notice("[Storage.Delivery] deliver", ansi_color: :light_magenta)
try do
- backend.store(endpoint, panel_info, data, meta_data)
+ backend.store(endpoint, data, meta_data)
rescue
e ->
Logger.error(Exception.format(:error, e, __STACKTRACE__))
@@ -41,11 +41,10 @@ defmodule Systems.Storage.Delivery do
%{
"backend" => backend,
"endpoint" => endpoint,
- "panel_info" => panel_info,
"data" => data,
"meta_data" => meta_data
} = _job
) do
- deliver(String.to_existing_atom(backend), endpoint, panel_info, data, meta_data)
+ deliver(String.to_existing_atom(backend), endpoint, data, meta_data)
end
end
diff --git a/core/systems/storage/content_page.ex b/core/systems/storage/endpoint_content_page.ex
similarity index 96%
rename from core/systems/storage/content_page.ex
rename to core/systems/storage/endpoint_content_page.ex
index eec174359..1fffd6bcd 100644
--- a/core/systems/storage/content_page.ex
+++ b/core/systems/storage/endpoint_content_page.ex
@@ -1,4 +1,4 @@
-defmodule Systems.Storage.ContentPage do
+defmodule Systems.Storage.EndpointContentPage do
use Systems.Content.Composer, :management_page
alias Systems.Storage
diff --git a/core/systems/storage/endpoint_content_page_builder.ex b/core/systems/storage/endpoint_content_page_builder.ex
new file mode 100644
index 000000000..fee5c40c6
--- /dev/null
+++ b/core/systems/storage/endpoint_content_page_builder.ex
@@ -0,0 +1,128 @@
+defmodule Systems.Storage.EndpointContentPageBuilder do
+ import CoreWeb.Gettext
+ import Frameworks.Utility.List
+
+ alias CoreWeb.UI.Timestamp
+ alias Systems.Storage
+
+ def view_model(
+ %{id: id} = endpoint,
+ assigns
+ ) do
+ show_errors = false
+ tabs = create_tabs(endpoint, show_errors, assigns)
+
+ %{
+ id: id,
+ title: get_title(endpoint),
+ tabs: tabs,
+ actions: [],
+ show_errors: show_errors,
+ active_menu_item: :projects
+ }
+ end
+
+ defp get_title(endpoint) do
+ special = Storage.EndpointModel.special_field(endpoint)
+ Storage.ServiceIds.translate(special)
+ end
+
+ defp get_tab_keys(endpoint) do
+ special = Storage.EndpointModel.special_field(endpoint)
+
+ []
+ |> append_if(:settings, special != :builtin)
+ |> append_if(:data, special == :builtin)
+ |> append(:monitor)
+ end
+
+ defp create_tabs(
+ endpoint,
+ show_errors,
+ assigns
+ ) do
+ get_tab_keys(endpoint)
+ |> Enum.map(&create_tab(&1, endpoint, show_errors, assigns))
+ end
+
+ defp create_tab(
+ :settings,
+ endpoint,
+ show_errors,
+ %{fabric: fabric} = _assigns
+ ) do
+ ready? = false
+
+ child =
+ Fabric.prepare_child(fabric, :settings_view, Storage.EndpointSettingsView, %{
+ endpoint: endpoint
+ })
+
+ %{
+ id: :settings_view,
+ ready: ready?,
+ show_errors: show_errors,
+ title: dgettext("eyra-storage", "tabbar.item.settings"),
+ forward_title: dgettext("eyra-storage", "tabbar.item.settings.forward"),
+ type: :fullpage,
+ child: child
+ }
+ end
+
+ defp create_tab(
+ :data,
+ endpoint,
+ show_errors,
+ %{fabric: fabric, timezone: timezone} = _assigns
+ ) do
+ ready? = false
+
+ files =
+ endpoint
+ |> Storage.Public.list_files()
+ |> Enum.map(fn %{timestamp: timestamp} = file ->
+ %{file | timestamp: Timestamp.convert(timestamp, timezone)}
+ end)
+
+ child =
+ Fabric.prepare_child(fabric, :data_view, Storage.EndpointDataView, %{
+ endpoint: endpoint,
+ files: files,
+ timezone: timezone
+ })
+
+ %{
+ id: :data_view,
+ ready: ready?,
+ show_errors: show_errors,
+ title: dgettext("eyra-storage", "tabbar.item.data"),
+ forward_title: dgettext("eyra-storage", "tabbar.item.data.forward"),
+ type: :fullpage,
+ child: child
+ }
+ end
+
+ defp create_tab(
+ :monitor,
+ endpoint,
+ show_errors,
+ %{fabric: fabric} = _assigns
+ ) do
+ ready? = false
+
+ child =
+ Fabric.prepare_child(fabric, :monitor_view, Storage.EndpointMonitorView, %{
+ endpoint: endpoint
+ })
+
+ %{
+ id: :monitor_view,
+ ready: ready?,
+ show_errors: show_errors,
+ title: dgettext("eyra-storage", "tabbar.item.monitor"),
+ forward_title: dgettext("eyra-storage", "tabbar.item.monitor.forward"),
+ type: :fullpage,
+ child: child
+ }
+ end
+end
diff --git a/core/systems/storage/endpoint_data_view.ex b/core/systems/storage/endpoint_data_view.ex
new file mode 100644
index 000000000..828dfcdeb
--- /dev/null
+++ b/core/systems/storage/endpoint_data_view.ex
@@ -0,0 +1,31 @@
+defmodule Systems.Storage.EndpointDataView do
+ use CoreWeb, :live_component
+
+ alias Systems.Storage.Html
+
+ @impl true
+ def update(%{endpoint: endpoint, files: files}, %{assigns: %{}} = socket) do
+ {
+ :ok,
+ socket
+ |> assign(
+ endpoint: endpoint,
+ files: files
+ )
+ }
+ end
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+
+
+ <%= dgettext("eyra-storage", "tabbar.item.data") %>
+ <.spacing value="L" />
+
+
+
+ """
+ end
+end
diff --git a/core/systems/storage/endpoint_form.ex b/core/systems/storage/endpoint_form.ex
index da7a3c410..c67116564 100644
--- a/core/systems/storage/endpoint_form.ex
+++ b/core/systems/storage/endpoint_form.ex
@@ -103,7 +103,7 @@ defmodule Systems.Storage.EndpointForm do
}
} = socket
) do
- changeset = Storage.EndpointModel.reset_special(endpoint, special_type, special_changeset)
+ changeset = Storage.EndpointModel.change_special(endpoint, special_type, special_changeset)
socket
|> send_event(:parent, "update", %{changeset: changeset})
diff --git a/core/systems/storage/endpoint_model.ex b/core/systems/storage/endpoint_model.ex
index ae2d8752f..aacbaf3e9 100644
--- a/core/systems/storage/endpoint_model.ex
+++ b/core/systems/storage/endpoint_model.ex
@@ -48,7 +48,7 @@ defmodule Systems.Storage.EndpointModel do
|> validate_required(@required_fields)
end
- def preload_graph(:down), do: @special_fields
+ def preload_graph(:down), do: @special_fields ++ [:auth_node]
def auth_tree(%{auth_node: auth_node}), do: auth_node
@@ -56,7 +56,7 @@ defmodule Systems.Storage.EndpointModel do
dgettext("eyra-storage", "project.item.tag")
end
- def reset_special(endpoint, special_field, special) when is_atom(special_field) do
+ def change_special(endpoint, special_field, special) when is_atom(special_field) do
specials =
Enum.map(
@special_fields,
@@ -121,6 +121,10 @@ defmodule Systems.Storage.EndpointModel do
defp map_to_field_id(field), do: String.to_existing_atom("#{field}_id")
+ defimpl Frameworks.GreenLight.AuthorizationNode do
+ def id(endpoint), do: endpoint.auth_node_id
+ end
+
defimpl Frameworks.Concept.ContentModel do
alias Systems.Storage
def form(_), do: Storage.EndpointForm
diff --git a/core/systems/storage/endpoint_monitor_view.ex b/core/systems/storage/endpoint_monitor_view.ex
new file mode 100644
index 000000000..0693755f7
--- /dev/null
+++ b/core/systems/storage/endpoint_monitor_view.ex
@@ -0,0 +1,30 @@
+defmodule Systems.Storage.EndpointMonitorView do
+ use CoreWeb, :live_component
+
+ @impl true
+ def update(%{endpoint: endpoint}, %{assigns: %{}} = socket) do
+ {
+ :ok,
+ socket
+ |> assign(endpoint: endpoint)
+ }
+ end
+
+ @impl true
+ def handle_event("update", _payload, socket) do
+ {:noreply, socket}
+ end
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+
+
+ <%= dgettext("eyra-storage", "tabbar.item.monitor") %>
+ <.spacing value="L" />
+
+
+ """
+ end
+end
diff --git a/core/systems/storage/endpoint_settings_view.ex b/core/systems/storage/endpoint_settings_view.ex
new file mode 100644
index 000000000..5ba85f0bb
--- /dev/null
+++ b/core/systems/storage/endpoint_settings_view.ex
@@ -0,0 +1,52 @@
+defmodule Systems.Storage.EndpointSettingsView do
+ use CoreWeb, :live_component
+
+ alias Frameworks.Concept
+ alias Systems.Storage
+
+ @impl true
+ def update(%{endpoint: endpoint}, %{assigns: %{}} = socket) do
+ {
+ :ok,
+ socket
+ |> assign(endpoint: endpoint, key: "key")
+ |> update_special()
+ |> compose_child(:special_form)
+ }
+ end
+
+ defp update_special(%{assigns: %{endpoint: endpoint}} = socket) do
+ special = Storage.EndpointModel.special(endpoint)
+ assign(socket, special: special)
+ end
+
+ @impl true
+ def compose(:special_form, %{special: special, key: key}) do
+ %{
+ module: Concept.ContentModel.form(special),
+ params: %{
+ model: special,
+ key: key
+ }
+ }
+ end
+
+ @impl true
+ def handle_event("update", _payload, socket) do
+ {:noreply, socket}
+ end
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+
+
+ <%= dgettext("eyra-storage", "tabbar.item.settings") %>
+ <.spacing value="L" />
+ <.child name={:special_form} fabric={@fabric} />
+
+
+ """
+ end
+end
diff --git a/core/systems/storage/fake_backend.ex b/core/systems/storage/fake_backend.ex
index e6774f0ae..9c0931562 100644
--- a/core/systems/storage/fake_backend.ex
+++ b/core/systems/storage/fake_backend.ex
@@ -1,8 +1,12 @@
defmodule Systems.Storage.FakeBackend do
@behaviour Systems.Storage.Backend
- def store(_endpoint, _panel_info, data, _meta_data) do
+ def store(_endpoint, data, _meta_data) do
IO.puts("fake store: #{data}")
:ok
end
+
+ def list_files(_endpoint) do
+ {:ok, []}
+ end
end
diff --git a/core/systems/storage/html.ex b/core/systems/storage/html.ex
new file mode 100644
index 000000000..5080d599b
--- /dev/null
+++ b/core/systems/storage/html.ex
@@ -0,0 +1,33 @@
+defmodule Systems.Storage.Html do
+ use CoreWeb, :html
+
+ import Frameworks.Pixel.Table
+
+ attr(:files, :list, required: true)
+
+ def files_table(%{files: files} = assigns) do
+ head_cells = [
+ dgettext("eyra-storage", "table.file.label"),
+ dgettext("eyra-storage", "table.size.label"),
+ dgettext("eyra-storage", "table.date.label")
+ ]
+
+ layout = [
+ %{type: :text, width: "flex-1", align: "text-left"},
+ %{type: :text, width: "w-40", align: "text-center"},
+ %{type: :date, width: "w-60", align: "text-right"}
+ ]
+
+ rows =
+ files
+ |> Enum.map(fn %{path: path, size: size, timestamp: timestamp} ->
+ [path, size, timestamp]
+ end)
+
+ assigns = assign(assigns, head_cells: head_cells, layout: layout, rows: rows)
+
+ ~H"""
+ <.table layout={@layout} head_cells={@head_cells} rows={@rows} border={false} />
+ """
+ end
+end
diff --git a/core/systems/storage/yoda/backend.ex b/core/systems/storage/yoda/backend.ex
index 37ec9c0b5..b7241274b 100644
--- a/core/systems/storage/yoda/backend.ex
+++ b/core/systems/storage/yoda/backend.ex
@@ -11,25 +11,26 @@ defmodule Systems.Storage.Yoda.Backend do
"password" => password,
"url" => yoda_url
} = _endpoint,
- %{
- "participant" => participant
- } = _panel_info,
data,
%{
- "key" => key
+ identifier: identifier
} = _meta_data
) do
- folder = "participant-#{participant}"
- folder_url = url([yoda_url, folder])
+ filename = filename(identifier)
+ file_url = url([yoda_url, filename])
- file = "#{key}.json"
- file_url = url([yoda_url, folder, file])
+ {:ok, _} = Yoda.Client.upload_file(username, password, file_url, data)
+ end
- with {:ok, false} <- Yoda.Client.has_resource?(username, password, folder_url) do
- {:ok, _} = Yoda.Client.create_folder(username, password, folder_url)
- end
+ def list_files(_endpoint) do
+ Logger.error("Not yet implemented: files/4")
+ {:error, :not_implemented}
+ end
- {:ok, _} = Yoda.Client.upload_file(username, password, file_url, data)
+ defp filename(%{"identifier" => identifier}) do
+ identifier
+ |> Enum.map_join("_", fn [key, value] -> "#{key}-#{value}" end)
+ |> then(&"#{&1}.json")
end
defp url(components) do
diff --git a/core/test/systems/storage/builtin/backend_test.exs b/core/test/systems/storage/builtin/backend_test.exs
index 3c6f83b2c..5b10ab66b 100644
--- a/core/test/systems/storage/builtin/backend_test.exs
+++ b/core/test/systems/storage/builtin/backend_test.exs
@@ -22,67 +22,69 @@ defmodule Systems.Storage.BuiltIn.BackendTest do
describe "store/4" do
test "unknown folder" do
- assert {:error, :endpoint_key_missing} = Backend.store(%{}, %{}, "data", %{})
+ assert {:error, :endpoint_key_missing} = Backend.store(%{}, "data", %{})
end
test "unknown participant" do
- expect(MockSpecial, :store, fn _, identifier, data ->
- assert ["participant=?", _unix_timestamp] = identifier
+ expect(MockSpecial, :store, fn folder, filename, data ->
+ assert "assignment=1" = folder
+ assert ".json" = filename
assert "data" = data
:ok
end)
- assert :ok = Backend.store(%{"key" => "assignment=1"}, %{}, "data", %{})
+ assert :ok = Backend.store(%{"key" => "assignment=1"}, "data", %{"identifier" => []})
end
test "folder + participant" do
- expect(MockSpecial, :store, fn folder, identifier, _data ->
+ expect(MockSpecial, :store, fn folder, filename, _data ->
assert "assignment=1" = folder
- assert ["participant=1", _unix_timestamp] = identifier
+ assert "participant=1.json" = filename
:ok
end)
- assert :ok = Backend.store(%{"key" => "assignment=1"}, %{"participant" => 1}, "data", %{})
+ assert :ok =
+ Backend.store(%{"key" => "assignment=1"}, "data", %{
+ "identifier" => [[:participant, 1]]
+ })
end
test "folder + participant + meta key" do
- expect(MockSpecial, :store, fn folder, identifier, _data ->
+ expect(MockSpecial, :store, fn folder, filename, _data ->
assert "assignment=1" = folder
- assert ["participant=1", "session=1"] = identifier
+ assert "participant=1_session=1.json" = filename
:ok
end)
assert :ok =
- Backend.store(%{"key" => "assignment=1"}, %{"participant" => 1}, "data", %{
- "key" => "session=1"
+ Backend.store(%{"key" => "assignment=1"}, "data", %{
+ "identifier" => [[:participant, 1], [:session, 1]]
})
end
- test "folder + participant + meta key + group" do
- expect(MockSpecial, :store, fn folder, identifier, _data ->
+ test "folder + participant + meta key + source" do
+ expect(MockSpecial, :store, fn folder, filename, _data ->
assert "assignment=1" = folder
- assert ["participant=1", "source=apple", "session=1"] = identifier
+ assert "participant=1_session=1_source=apple.json" = filename
:ok
end)
assert :ok =
- Backend.store(%{"key" => "assignment=1"}, %{"participant" => 1}, "data", %{
- "key" => "session=1",
- "group" => "apple"
+ Backend.store(%{"key" => "assignment=1"}, "data", %{
+ "identifier" => [[:participant, 1], [:session, 1], [:source, "apple"]]
})
end
- test "folder + participant + meta key + group=nil" do
- expect(MockSpecial, :store, fn folder, identifier, _data ->
+ test "folder + participant + meta key + source=nil" do
+ expect(MockSpecial, :store, fn folder, filename, _data ->
assert "assignment=1" = folder
- assert ["participant=1", "session=1"] = identifier
+ assert "participant=1_session=1_source=.json" = filename
:ok
end)
assert :ok =
- Backend.store(%{"key" => "assignment=1"}, %{"participant" => 1}, "data", %{
- "key" => "session=1",
- "group" => nil
+ Backend.store(%{"key" => "assignment=1"}, "data", %{
+ "identifier" => [[:participant, 1], [:session, 1], [:source, nil]]
})
end
end