Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/fun_with_flags/ui/templates.ex
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ defmodule FunWithFlags.UI.Templates do
Calendar.strftime(dt, "%Y-%m-%d %H:%M:%S")
end

def format_utc_timestamp(%NaiveDateTime{} = ndt) do
Calendar.strftime(ndt, "%Y-%m-%d %H:%M:%S")
end

def format_utc_timestamp(nil), do: ""

def audit_log_page_path(conn, page, assigns) do
Expand Down
140 changes: 133 additions & 7 deletions lib/fun_with_flags/ui/templates/index.html.eex
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,26 @@
<div class="container mt-3">
<div class="row d-none d-md-flex">
<div class="col">
<table class="table table-hover">
<table class="table table-hover" id="fwf-flags-table">
<thead class="thead-default">
<tr>
<th>name</th>
<th>status</th>
<th class="fwf-sortable" data-sort-key="name" style="cursor: pointer; user-select: none;">
name <span class="fwf-sort-arrow" data-col="name"></span>
</th>
<th class="fwf-sortable" data-sort-key="status" style="cursor: pointer; user-select: none;">
status <span class="fwf-sort-arrow" data-col="status"></span>
</th>
<th>gates</th>
<th class="fwf-sortable" data-sort-key="created" style="cursor: pointer; user-select: none;">
created <span class="fwf-sort-arrow" data-col="created"></span>
</th>
</tr>
</thead>
<tbody>
<tbody id="fwf-flags-tbody">
<%= for flag <- @flags do %>
<tr>
<tr data-name="<%= html_escape(flag.name) %>"
data-status="<%= Utils.get_flag_status(flag) %>"
data-created="<%= if flag.created_at, do: format_utc_timestamp(flag.created_at), else: "" %>">
<td>
<a href="<%= path(@conn, "/flags/#{url_safe(flag.name)}") %>">
<%= html_escape(flag.name) %>
Expand All @@ -50,16 +59,23 @@
<td>
<%= html_gate_list(flag) %>
</td>

<td class="fwf-created-at">
🦖
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
<div class="container d-flex flex-column d-md-none">
<div class="container d-flex flex-column d-md-none" id="fwf-flags-cards">
<%= for flag <- @flags do %>
<div class="card mb-3">
<div class="card mb-3"
data-name="<%= html_escape(flag.name) %>"
data-status="<%= Utils.get_flag_status(flag) %>"
data-created="<%= if flag.created_at, do: format_utc_timestamp(flag.created_at), else: "" %>">
<div class="card-body">
<h5 class="card-title">
<a href="<%= path(@conn, "/flags/#{url_safe(flag.name)}") %>">
Expand All @@ -75,10 +91,120 @@
<strong>Gates:</strong>
<%= html_gate_list(flag) %>
</div>
<div>
<strong>Created:</strong>
<span class="fwf-created-at">🦖</span>
</div>
</div>
</div>
</div>
<% end %>
</div>
<script>
(function() {
var STATUS_ORDER = {fully_open: 0, half_open: 1, closed: 2};

function getSort() {
try { return localStorage.getItem('fwf-sort') || 'name_asc'; } catch(e) { return 'name_asc'; }
}

function setSort(s) {
try { localStorage.setItem('fwf-sort', s); } catch(e) {}
}

function compareName(a, b) {
return a.dataset.name.localeCompare(b.dataset.name);
}

function compareStatus(a, b) {
var sa = STATUS_ORDER[a.dataset.status] || 0;
var sb = STATUS_ORDER[b.dataset.status] || 0;
return sa - sb || compareName(a, b);
}

function compareCreated(a, b, dir) {
var ca = a.dataset.created;
var cb = b.dataset.created;
if (!ca && !cb) return compareName(a, b);
if (!ca) return 1;
if (!cb) return -1;
var r = ca < cb ? -1 : ca > cb ? 1 : 0;
return dir === 'desc' ? -r : r;
}

function sortElements(container, elements, sortKey) {
var parts = sortKey.match(/^(.+)_(asc|desc)$/);
if (!parts) return;
var col = parts[1], dir = parts[2];

var arr = Array.from(elements);
if (col === 'created') {
arr.sort(function(a, b) { return compareCreated(a, b, dir); });
} else {
var cmp = col === 'name' ? compareName : compareStatus;
arr.sort(function(a, b) {
var r = cmp(a, b);
return dir === 'desc' ? -r : r;
});
}
arr.forEach(function(el) { container.appendChild(el); });
}

function updateArrows(sortKey) {
var parts = sortKey.match(/^(.+)_(asc|desc)$/);
document.querySelectorAll('.fwf-sort-arrow').forEach(function(span) {
if (parts && span.dataset.col === parts[1]) {
span.innerHTML = parts[2] === 'asc' ? '&uarr;' : '&darr;';
} else {
span.innerHTML = '';
}
});
}

function applySort(sortKey) {
var tbody = document.getElementById('fwf-flags-tbody');
if (tbody) sortElements(tbody, tbody.querySelectorAll('tr'), sortKey);

var cards = document.getElementById('fwf-flags-cards');
if (cards) sortElements(cards, cards.querySelectorAll('.card'), sortKey);

updateArrows(sortKey);
}

// Format UTC timestamps to local time
document.querySelectorAll('[data-created]').forEach(function(el) {
var utc = el.dataset.created;
if (utc) {
var cell = el.querySelector('.fwf-created-at');
if (cell) {
var d = new Date(utc + ' UTC');
var day = d.getDate();
var month = d.toLocaleString(undefined, {month: 'short'});
var year = d.getFullYear();
var hour = d.getHours();
var ampm = hour >= 12 ? 'PM' : 'AM';
hour = hour % 12 || 12;
var min = d.getMinutes().toString().padStart(2, '0');
cell.textContent = day + ' ' + month + ' ' + year + ' ' + hour + ':' + min + ' ' + ampm;
}
}
});

// Apply stored sort
var current = getSort();
applySort(current);

// Click handlers
document.querySelectorAll('.fwf-sortable').forEach(function(th) {
th.addEventListener('click', function() {
var col = this.dataset.sortKey;
var cur = getSort();
var next = (cur === col + '_asc') ? col + '_desc' : col + '_asc';
setSort(next);
applySort(next);
});
});
})();
</script>
</body>
</html>
23 changes: 2 additions & 21 deletions lib/fun_with_flags/ui/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,29 +34,9 @@ defmodule FunWithFlags.UI.Utils do


def sort_flags(flags) do
Enum.sort(flags, &sorter/2)
Enum.sort_by(flags, & &1.name)
end

defp sorter(a, b) do
sa = get_flag_status(a)
sb = get_flag_status(b)

if sa == sb do
a.name < b.name
else
case sa do
:fully_open -> true
:half_open ->
case sb do
:fully_open -> false
:closed -> true
end
:closed -> false
end
end
end


# Create new flags as disabled.
#
# Here we are converting a user-provided string to an atom, which is
Expand Down Expand Up @@ -233,4 +213,5 @@ defmodule FunWithFlags.UI.Utils do
|> List.last()
|> String.length()
end

end
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ defmodule FunWithFlagsUi.Mixfile do
{:plug_cowboy, ">= 2.0.0", optional: true},
{:cowboy, ">= 2.0.0", optional: true},
# {:fun_with_flags, path: "../fun_with_flags"},
{:fun_with_flags, git: "https://github.com/invideoio/fun_with_flags.git", branch: "master"},
{:fun_with_flags, git: "https://github.com/invideoio/fun_with_flags.git", branch: "feat/impr"},
{:redix, "~> 1.0", only: [:dev, :test]},
{:ex_doc, ">= 0.0.0", only: :dev},
{:credo, "~> 1.7", only: :dev, runtime: false},
Expand Down
2 changes: 1 addition & 1 deletion mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
"ex_doc": {:hex, :ex_doc, "0.38.4", "ab48dff7a8af84226bf23baddcdda329f467255d924380a0cf0cee97bb9a9ede", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "f7b62346408a83911c2580154e35613eb314e0278aeea72ed7fedef9c1f165b2"},
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
"fun_with_flags": {:git, "https://github.com/invideoio/fun_with_flags.git", "dd3bbe1b483afa2de2f9484f393d2f1ed9790e4a", [branch: "master"]},
"fun_with_flags": {:git, "https://github.com/invideoio/fun_with_flags.git", "71c652ab15b59678fe003c9f7aa81a0be55e4e15", [branch: "feat/impr"]},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
Expand Down
Loading