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
- <%= if @centralize do %> -
-
- <%= render_slot(@inner_block) %> -
- <% else %> -
- <%= render_slot(@inner_block) %> -
- <% end %> +
+ <%= render_slot(@inner_block) %> +
<%= if @has_right_bar_buttons do %> <%= if not @hide_seperator do %>
@@ -94,9 +80,6 @@ defmodule Frameworks.Pixel.Navigation do
<% end %> - <%= if @centralize do %> -
- <% end %>
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""" +
+ + + <.row top={true} cells={@head_cells} layout={@layout} border={@border}/> + <%= for {cells, index} <- Enum.with_index(@rows) do %> + <.row + bottom={index == Enum.count(@rows)-1} + cells={cells} + layout={@layout} + border={@border} + /> + <% end %> + +
+
+ """ + end + + attr(:cells, :list, required: true) + attr(:layout, :list, required: true) + attr(:top, :boolean, default: false) + attr(:bottom, :boolean, default: false) + attr(:border, :boolean, default: true) + + 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} + border={@border} + /> + <% end %> + + """ + end + + defp cell_padding(:top, %{border: true, top: true}), do: "pt-2" + defp cell_padding(:left, %{border: true, left: true}), do: "pl-6" + defp cell_padding(:right, %{border: true, right: true}), do: "pr-6" + defp cell_padding(_, _), do: "" + + 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) + attr(:border, :boolean, default: true) + + 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 = + [:top, :left, :right] + |> Enum.map_join(" ", &cell_padding(&1, assigns)) + + assigns = assign(assigns, %{padding: padding, layout: layout}) + + ~H""" + +
+ <%= if @top do %> + <%= @content %> + <% else %> + + <.content type={@layout.type} value={@content} /> + + <% end %> +
+ + """ + end + + attr(:type, :atom, required: true) + attr(:value, :string, required: true) + + def content(%{type: :href} = assigns) do + ~H""" + Link + """ + end + + def content(%{type: :date} = assigns) do + ~H""" + <%= Timestamp.stamp(@value) %> + """ + end + + def content(assigns) do + ~H""" + <%= @value %> + """ + end + + defp valid_url?(string) do + uri = URI.parse(string) + uri.scheme != nil && uri.host =~ "." + end +end diff --git a/core/frameworks/pixel/components/text.ex b/core/frameworks/pixel/components/text.ex index 344ad5340..4e963111c 100644 --- a/core/frameworks/pixel/components/text.ex +++ b/core/frameworks/pixel/components/text.ex @@ -158,22 +158,24 @@ defmodule Frameworks.Pixel.Text do end attr(:color, :string, default: "text-grey1") + attr(:align, :string, default: "text-left") slot(:inner_block, required: true) def table_head(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