Packages
livebook
0.1.0
0.19.8
0.19.7
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.14.0-rc.1
0.14.0-rc.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
Automate code & data workflows with interactive notebooks
Current section
Files
Jump to
Current section
Files
lib/livebook_web/live/path_select_component.ex
defmodule LivebookWeb.PathSelectComponent do
use LivebookWeb, :live_component
# The component expects:
#
# * `path` - the currently entered path
# * `running_paths` - the list of notebook paths that are already linked to running sessions
# * `extnames` - a list of file extensions that should be shown
# * `phx_target` - id of the component to send update events to or nil to send to the parent LV
# * `phx_submit` - the event name sent on form submission, use `nil` for no action
#
# The target receives `set_path` events with `%{"path" => path}` payload.
#
# Optionally inner block may be passed (e.g. with action buttons)
# and it's rendered next to the text input.
#
# To force the component refetch the displayed files
# you can `send_update` with `force_reload: true` to the component.
@impl true
def mount(socket) do
inner_block = Map.get(socket.assigns, :inner_block, nil)
{:ok, assign(socket, inner_block: inner_block, current_dir: nil)}
end
@impl true
def update(assigns, socket) do
{force_reload?, assigns} = Map.pop(assigns, :force_reload, false)
%{assigns: assigns} = socket = assign(socket, assigns)
{dir, basename} = split_path(assigns.path)
dir = Path.expand(dir)
files =
if assigns.current_dir != dir or force_reload? do
list_files(dir, assigns.extnames, assigns.running_paths)
else
assigns.files
end
{:ok, assign(socket, files: annotate_matching(files, basename), current_dir: dir)}
end
@impl true
def render(assigns) do
~L"""
<div class="h-full flex flex-col">
<div class="flex space-x-5 items-center mb-4">
<form class="flex-grow"
phx-change="set_path"
<%= if @phx_submit do %>
phx-submit="<%= @phx_submit %>"
<% else %>
onsubmit="return false"
<% end %>
<%= if @phx_target, do: "phx-target=#{@phx_target}" %>>
<input class="input"
id="input-path"
phx-hook="FocusOnUpdate"
type="text"
name="path"
placeholder="File"
value="<%= @path %>"
spellcheck="false"
autocomplete="off" />
</form>
<%= if @inner_block do %>
<div>
<%= render_block(@inner_block) %>
</div>
<% end %>
</div>
<div class="flex-grow -m-1 p-1 overflow-y-auto tiny-scrollbar" tabindex="-1">
<%= if highlighting?(@files) do %>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2 border-b border-dashed border-grey-200 mb-2 pb-2">
<%= for file <- @files, file.highlighted != "" do %>
<%= render_file(file, @phx_target) %>
<% end %>
</div>
<% end %>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2">
<%= for file <- @files, file.highlighted == "" do %>
<%= render_file(file, @phx_target) %>
<% end %>
</div>
</div>
</div>
"""
end
defp highlighting?(files) do
Enum.any?(files, &(&1.highlighted != ""))
end
defp render_file(file, phx_target) do
icon =
case file do
%{is_running: true} -> "play-circle-line"
%{is_dir: true} -> "folder-fill"
_ -> "file-code-line"
end
assigns = %{file: file, icon: icon}
~L"""
<button class="flex space-x-2 items-center p-2 rounded-lg hover:bg-gray-100 focus:ring-1 focus:ring-gray-400"
phx-click="set_path"
phx-value-path="<%= @file.path %>"
<%= if phx_target, do: "phx-target=#{phx_target}" %>>
<span class="block">
<%= remix_icon(@icon, class: "text-xl align-middle #{if(@file.is_running, do: "text-green-300", else: "text-gray-400")}") %>
</span>
<span class="flex font-medium overflow-hidden overflow-ellipsis whitespace-nowrap <%= if(@file.is_running, do: "text-green-300", else: "text-gray-500") %>">
<%= if @file.highlighted != "" do %>
<span class="font-medium <%= if(@file.is_running, do: "text-green-400", else: "text-gray-900") %>">
<%= @file.highlighted %>
</span>
<% end %>
<span>
<%= @file.unhighlighted %>
</span>
</span>
</button>
"""
end
defp annotate_matching(files, prefix) do
for %{name: name} = file <- files do
if String.starts_with?(name, prefix) do
%{file | highlighted: prefix, unhighlighted: String.replace_prefix(name, prefix, "")}
else
%{file | highlighted: "", unhighlighted: name}
end
end
end
defp list_files(dir, extnames, running_paths) do
if File.exists?(dir) do
file_names =
case File.ls(dir) do
{:ok, names} -> names
{:error, _} -> []
end
file_infos =
file_names
|> Enum.map(fn name ->
file_info(name, Path.join(dir, name), running_paths)
end)
|> Enum.filter(fn file ->
not hidden?(file.name) and (file.is_dir or valid_extension?(file.name, extnames))
end)
parent = Path.dirname(dir)
file_infos =
if parent == dir do
file_infos
else
[file_info("..", parent, running_paths) | file_infos]
end
Enum.sort_by(file_infos, fn file -> {!file.is_dir, file.name} end)
else
[]
end
end
defp file_info(name, path, running_paths) do
is_dir = File.dir?(path)
%{
name: name,
highlighted: "",
unhighlighted: name,
path: if(is_dir, do: ensure_trailing_slash(path), else: path),
is_dir: is_dir,
is_running: path in running_paths
}
end
defp hidden?(filename) do
String.starts_with?(filename, ".")
end
defp valid_extension?(filename, extnames) do
Path.extname(filename) in extnames
end
# Note: to provide an intuitive behavior when typing the path
# we enter a new directory when it has a trailing slash,
# so given "/foo/bar" we list files in "foo" and given "/foo/bar/
# we list files in "bar".
#
# The basename is kinda like search within the current directory,
# so we highlight files starting with that string.
defp split_path(path) do
if String.ends_with?(path, "/") do
{path, ""}
else
{Path.dirname(path), Path.basename(path)}
end
end
defp ensure_trailing_slash(path) do
if String.ends_with?(path, "/") do
path
else
path <> "/"
end
end
end