Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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.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", "1c924332c1b0ec13e8440730767c75e1bb23f300", [branch: "master"]},
"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