Update dashboard icons

This commit is contained in:
Pranav
2025-12-19 12:14:23 -08:00
parent 05f9c3053d
commit b1c4ac7ede
26 changed files with 421 additions and 234 deletions
@@ -1,11 +1,41 @@
class SuperAdmin::DashboardController < SuperAdmin::ApplicationController
include ActionView::Helpers::NumberHelper
ALLOWED_CHART_DAYS = [7, 30].freeze
def index
@data = Conversation.unscoped.group_by_day(:created_at, range: 30.days.ago..2.seconds.ago).count.to_a
@accounts_count = number_with_delimiter(Account.count)
@users_count = number_with_delimiter(User.count)
@inboxes_count = number_with_delimiter(Inbox.count)
@conversations_count = number_with_delimiter(Conversation.count)
@chart_days = parse_chart_days
@data = conversations_by_day
@accounts_count = format_count(Account.count)
@users_count = format_count(User.count)
@inboxes_count = format_count(Inbox.count)
@conversations_count = format_count(recent_conversations.count)
end
private
def parse_chart_days
days = params[:chart_period].to_i
ALLOWED_CHART_DAYS.include?(days) ? days : 7
end
def chart_date_range
(@chart_days - 1).days.ago.beginning_of_day..Time.current
end
def last_30_days
29.days.ago.beginning_of_day..Time.current
end
def recent_conversations
Conversation.unscoped.where(created_at: last_30_days)
end
def conversations_by_day
Conversation.unscoped.group_by_day(:created_at, range: chart_date_range, default_value: 0).count.to_a
end
def format_count(count)
number_with_delimiter(count)
end
end
+4 -5
View File
@@ -31,8 +31,8 @@ class AccountDashboard < Administrate::BaseDashboard
updated_at: Field::DateTime,
users: CountField,
conversations: CountField,
locale: Field::Select.with_options(collection: LANGUAGES_CONFIG.map { |_x, y| y[:iso_639_1_code] }),
status: Field::Select.with_options(collection: [%w[Active active], %w[Suspended suspended]]),
locale: LocaleField,
status: StatusField,
account_users: Field::HasMany,
custom_attributes: Field::String
}.merge(enterprise_attribute_types).freeze
@@ -45,10 +45,9 @@ class AccountDashboard < Administrate::BaseDashboard
COLLECTION_ATTRIBUTES = %i[
id
name
locale
users
conversations
status
users
locale
].freeze
# SHOW_PAGE_ATTRIBUTES
+12
View File
@@ -0,0 +1,12 @@
require 'administrate/field/base'
class LocaleField < Administrate::Field::Base
def to_s
language = LANGUAGES_CONFIG.find { |_key, val| val[:iso_639_1_code] == data }
language ? language[1][:name] : data
end
def selectable_options
LANGUAGES_CONFIG.map { |_key, val| [val[:name], val[:iso_639_1_code]] }
end
end
+15
View File
@@ -0,0 +1,15 @@
require 'administrate/field/base'
class StatusField < Administrate::Field::Base
def to_s
data.to_s.capitalize
end
def active?
data.to_s == 'active'
end
def selectable_options
[%w[Active active], %w[Suspended suspended]]
end
end
@@ -1,6 +1,8 @@
<script setup>
import { computed } from 'vue';
/* eslint-disable vue/no-bare-strings-in-template */
import { computed, ref } from 'vue';
import BarChart from 'shared/components/charts/BarChart.vue';
const props = defineProps({
componentData: {
type: Object,
@@ -8,11 +10,32 @@ const props = defineProps({
},
});
const chartFilterOptions = [
{ value: '7', label: 'Last 7d' },
{ value: '30', label: 'Last 30d' },
];
const selectedChartFilter = ref(
props.componentData.chartDays?.toString() || '7'
);
const onChartFilterChange = event => {
const period = event.target.value;
const url = new URL(window.location.href);
url.searchParams.set('chart_period', period);
window.location.href = url.toString();
};
const formatDate = dateString => {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
};
const prepareData = sourceData => {
var labels = [];
var data = [];
const labels = [];
const data = [];
sourceData.forEach(item => {
labels.push(item[0]);
labels.push(formatDate(item[0]));
data.push(item[1]);
});
return {
@@ -44,18 +67,19 @@ const { accountsCount, usersCount, inboxesCount, conversationsCount } =
role="banner"
>
<div
class="flex items-center justify-between w-full h-20 mx-auto max-w-[60rem]"
class="flex items-center justify-between w-full h-16 mx-auto max-w-[60rem]"
>
<div class="flex items-center gap-3">
<div
class="border border-n-weak p-2 rounded-full text-n-slate-11"
>
<div class="border border-n-weak p-2 rounded-full text-n-slate-11">
<svg width="24" height="24">
<use xlink:href="#icon-dashboard-line" />
<use xlink:href="#icon-layout-dashboard" />
</svg>
</div>
<div class="flex flex-col justify-center">
<h1 id="page-title" class="text-base font-medium tracking-tight text-n-slate-12">
<h1
id="page-title"
class="text-base font-medium tracking-tight text-n-slate-12"
>
Admin Dashboard
</h1>
<p class="text-xs text-n-slate-11 m-0">
@@ -67,7 +91,7 @@ const { accountsCount, usersCount, inboxesCount, conversationsCount } =
</header>
<main class="flex-1 px-6 overflow-y-auto">
<div class="w-full mx-auto max-w-[60rem] py-4">
<div class="w-full mx-auto max-w-[60rem] py-8">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
<div
class="p-4 rounded-xl bg-n-solid-2 outline outline-1 outline-n-container shadow-sm"
@@ -99,14 +123,34 @@ const { accountsCount, usersCount, inboxesCount, conversationsCount } =
<div class="text-3xl font-semibold text-n-slate-12">
{{ conversationsCount }}
</div>
<div class="text-sm text-n-slate-11">Conversations</div>
<div class="text-sm text-n-slate-11">Conversations (30d)</div>
</div>
</div>
<div
class="p-6 rounded-xl bg-n-solid-2 outline outline-1 outline-n-container shadow-sm"
class="px-6 pt-4 pb-6 rounded-xl bg-n-solid-2 outline outline-1 outline-n-container shadow-sm"
>
<BarChart class="w-full max-h-[500px]" :collection="chartData" />
<div class="flex items-center justify-between mb-4">
<div class="text-base font-semibold text-n-slate-12">
Conversations
</div>
<select
v-model="selectedChartFilter"
class="w-auto px-3 py-1.5 text-sm rounded-lg border border-n-weak bg-n-background text-n-slate-12 cursor-pointer focus:outline-none focus:ring-2 focus:ring-n-brand"
@change="onChartFilterChange"
>
<option
v-for="option in chartFilterOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</div>
<div class="h-[300px]">
<BarChart class="w-full h-full" :collection="chartData" />
</div>
</div>
</div>
</main>
@@ -1 +1 @@
<%= image_tag field.avatar_url if field.avatar_url.present? %>
<%= image_tag field.avatar_url, class: "w-8 h-8 rounded-full" if field.avatar_url.present? %>
@@ -0,0 +1,9 @@
<div class="flex flex-col gap-1">
<%= f.label field.attribute, class: "text-sm font-medium text-n-slate-11" %>
<%= f.select(
field.attribute,
options_for_select(field.selectable_options, field.data),
{ include_blank: false },
{ class: "h-9 px-3 min-w-[120px] text-sm text-n-slate-12 bg-n-solid-2 border border-n-weak rounded-lg focus:ring-2 focus:ring-woot-500 focus:border-woot-500 outline-none" }
) %>
</div>
@@ -0,0 +1,3 @@
<span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium bg-n-alpha-2 text-n-slate-11">
<%= field.to_s %>
</span>
@@ -0,0 +1 @@
<%= field.to_s %>
@@ -0,0 +1,9 @@
<div class="flex flex-col gap-1">
<%= f.label field.attribute, class: "text-sm font-medium text-n-slate-11" %>
<%= f.select(
field.attribute,
options_for_select(field.selectable_options, field.data),
{ include_blank: false },
{ class: "h-9 px-3 min-w-[120px] text-sm text-n-slate-12 bg-n-solid-2 border border-n-weak rounded-lg focus:ring-2 focus:ring-woot-500 focus:border-woot-500 outline-none" }
) %>
</div>
@@ -0,0 +1,3 @@
<span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium <%= field.active? ? 'bg-n-teal-3 text-n-teal-11' : 'bg-n-ruby-3 text-n-ruby-11' %>">
<%= field.to_s %>
</span>
@@ -0,0 +1,3 @@
<span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium <%= field.active? ? 'bg-n-teal-3 text-n-teal-11' : 'bg-n-ruby-3 text-n-ruby-11' %>">
<%= field.to_s %>
</span>
+14 -14
View File
@@ -21,10 +21,10 @@ end %>
<section class="flex flex-col w-full h-full">
<header class="sticky top-0 z-10 bg-n-background border-b border-n-weak" role="banner">
<div class="flex items-center justify-between w-full h-20 mx-auto max-w-[60rem]">
<div class="flex items-center justify-between w-full h-16 mx-auto max-w-[60rem]">
<div class="flex items-center gap-3">
<div class="border border-n-weak p-2 rounded-full text-n-slate-11">
<svg width="24" height="24"><use xlink:href="#icon-building-4-line" /></svg>
<svg width="24" height="24"><use xlink:href="#icon-building-2" /></svg>
</div>
<div class="flex flex-col justify-center">
<h1 class="text-base font-medium tracking-tight text-n-slate-12">
@@ -82,38 +82,38 @@ end %>
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-n-slate-1">
<thead class="border-b border-n-weak">
<tr>
<th class="text-left py-3 px-6 text-sm font-medium text-n-slate-11">Account</th>
<th class="text-left py-3 px-6 text-sm font-medium text-n-slate-11">User</th>
<th class="text-left py-3 px-6 text-sm font-medium text-n-slate-11">Inviter</th>
<th class="text-left py-3 px-6 text-sm font-medium text-n-slate-11">Role</th>
<th class="text-left py-3 px-6 text-sm font-medium text-n-slate-11"></th>
<th class="text-left py-3 !pl-6 pr-4 text-sm text-n-slate-11">Account</th>
<th class="text-left py-3 px-4 text-sm text-n-slate-11">User</th>
<th class="text-left py-3 px-4 text-sm text-n-slate-11">Inviter</th>
<th class="text-left py-3 px-4 text-sm text-n-slate-11">Role</th>
<th class="text-left py-3 pl-4 !pr-6 text-sm text-n-slate-11"></th>
</tr>
</thead>
<tbody class="divide-y divide-n-weak">
<% page.resource.account_users.each do |account_user| %>
<tr class="hover:bg-n-alpha-1">
<td class="py-3 px-6 text-sm text-n-slate-12">
<td class="py-4 !pl-6 pr-4 text-sm text-n-slate-12">
<%= link_to "##{account_user.account_id} #{account_user.account.name}", super_admin_account_path(account_user.account), class: "text-n-slate-12 hover:underline" %>
</td>
<td class="py-3 px-6 text-sm text-n-slate-12">
<td class="py-4 px-4 text-sm text-n-slate-12">
<%= link_to "##{account_user.user_id} #{account_user.user.name}", super_admin_user_path(account_user.user), class: "text-n-slate-12 hover:underline" %>
</td>
<td class="py-3 px-6 text-sm text-n-slate-11">
<td class="py-4 px-4 text-sm text-n-slate-11">
<% if account_user.inviter.present? %>
<%= link_to account_user.inviter.name, super_admin_user_path(account_user.inviter), class: "text-n-slate-12 hover:underline" %>
<% else %>
<span class="text-n-slate-10">---</span>
<% end %>
</td>
<td class="py-3 px-6">
<td class="py-4 px-4">
<span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium bg-n-alpha-2 text-n-slate-12 capitalize">
<%= account_user.role %>
</span>
</td>
<td class="py-3 px-6 text-right">
<%= link_to "Destroy", super_admin_account_user_path(account_user), method: :delete, data: { confirm: "Are you sure?" }, class: "text-sm text-ruby-500 hover:text-ruby-600 hover:underline" %>
<td class="py-4 pl-4 !pr-6 text-right">
<%= link_to "Destroy", super_admin_account_user_path(account_user), method: :delete, data: { confirm: "Are you sure?" }, class: "text-sm font-medium text-n-ruby-11 hover:underline" %>
</td>
</tr>
<% end %>
@@ -10,10 +10,10 @@
<section class="flex flex-col w-full h-full">
<header class="sticky top-0 z-10 bg-n-background border-b border-n-weak" role="banner">
<div class="flex items-center justify-between w-full h-20 mx-auto max-w-[60rem]">
<div class="flex items-center justify-between w-full h-16 mx-auto max-w-[60rem]">
<div class="flex items-center gap-3">
<div class="border border-n-weak p-2 rounded-full text-n-slate-11">
<svg width="24" height="24"><use xlink:href="#icon-settings-2-line" /></svg>
<svg width="24" height="24"><use xlink:href="#icon-zap" /></svg>
</div>
<div class="flex flex-col justify-center">
<h1 class="text-base font-medium tracking-tight text-n-slate-12" id="page-title">
@@ -18,80 +18,80 @@ to display a collection of resources in an HTML table.
[1]: http://www.rubydoc.info/gems/administrate/Administrate/Page/Collection
%>
<div class="rounded-xl bg-n-solid-2 shadow outline-1 outline outline-n-container overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full" aria-labelledby="<%= table_title %>">
<thead class="bg-n-slate-1">
<tr>
<% collection_presenter.attribute_types.each do |attr_name, attr_type| %>
<th class="text-left py-3 px-4 text-sm font-medium text-n-slate-11"
scope="col"
role="columnheader"
aria-sort="<%= sort_order(collection_presenter.ordered_html_class(attr_name)) %>">
<%= link_to(sanitized_order_params(page, collection_field_name).merge(
collection_presenter.order_params_for(attr_name, key: collection_field_name)
), class: "inline-flex items-center gap-1 hover:text-n-slate-12") do %>
<%= t(
"helpers.label.#{collection_presenter.resource_name}.#{attr_name}",
default: attr_name.to_s,
).titleize %>
<% if collection_presenter.ordered_by?(attr_name) %>
<span class="<%= collection_presenter.ordered_html_class(attr_name) == 'asc' ? '' : 'rotate-180' %>">
<svg class="w-3 h-3" aria-hidden="true">
<use xlink:href="#icon-up-caret" />
</svg>
</span>
<% end %>
<div class="overflow-x-auto">
<table class="w-full" aria-labelledby="<%= table_title %>">
<thead class="border-b border-n-weak">
<tr>
<% collection_presenter.attribute_types.each_with_index do |(attr_name, attr_type), index| %>
<th class="text-left py-3 px-4 text-sm text-n-slate-11 first:!pl-3 last:!pr-3"
scope="col"
role="columnheader"
aria-sort="<%= sort_order(collection_presenter.ordered_html_class(attr_name)) %>">
<%= link_to(sanitized_order_params(page, collection_field_name).merge(
collection_presenter.order_params_for(attr_name, key: collection_field_name)
), class: "inline-flex items-center gap-1 hover:text-n-slate-12") do %>
<%= t(
"helpers.label.#{collection_presenter.resource_name}.#{attr_name}",
default: attr_name.to_s,
).titleize %>
<% if collection_presenter.ordered_by?(attr_name) %>
<span class="<%= collection_presenter.ordered_html_class(attr_name) == 'asc' ? '' : 'rotate-180' %>">
<svg class="w-3 h-3" aria-hidden="true">
<use xlink:href="#icon-up-caret" />
</svg>
</span>
<% end %>
</th>
<% end %>
</th>
<% end %>
<% if existing_action?(collection_presenter.resource_name, :edit) %>
<th scope="col"></th>
<% end %>
<% if existing_action?(collection_presenter.resource_name, :destroy) %>
<th scope="col" class="pr-3"></th>
<% end %>
</tr>
</thead>
<tbody class="divide-y divide-n-weak">
<% resources.each do |resource| %>
<tr class="js-table-row hover:bg-n-alpha-1 transition-colors"
tabindex="0"
<% if existing_action? collection_presenter.resource_name, :show %>
<%= %(role=link data-url=#{polymorphic_path([namespace, resource])}) %>
<% end %>
>
<% collection_presenter.attributes_for(resource).each_with_index do |attribute, index| %>
<td class="py-4 px-4 text-sm text-n-slate-12 first:!pl-3 last:!pr-3">
<% if authorized_action? resource, :show -%>
<a href="<%= polymorphic_path([namespace, resource]) -%>"
class="hover:underline"
>
<%= render_field attribute %>
</a>
<% end -%>
</td>
<% end %>
<% [existing_action?(collection_presenter.resource_name, :edit),
existing_action?(collection_presenter.resource_name, :destroy)].count(true).times do %>
<th scope="col" class="py-3 px-4"></th>
<% if existing_action?(collection_presenter.resource_name, :edit) %>
<td class="py-4 px-4 text-right"><%= link_to(
t("administrate.actions.edit"),
[:edit, namespace, resource],
class: "text-sm font-medium text-n-brand hover:underline",
) if authorized_action? resource, :edit%></td>
<% end %>
<% if existing_action?(collection_presenter.resource_name, :destroy) %>
<td class="py-4 px-4 !pr-3 text-right"><%= link_to(
t("administrate.actions.destroy"),
[namespace, resource],
class: "text-sm font-medium text-n-ruby-11 hover:underline",
method: :delete,
data: { confirm: t("administrate.actions.confirm") }
) if authorized_action? resource, :destroy %></td>
<% end %>
</tr>
</thead>
<tbody class="divide-y divide-n-weak">
<% resources.each do |resource| %>
<tr class="js-table-row hover:bg-n-alpha-1 transition-colors"
tabindex="0"
<% if existing_action? collection_presenter.resource_name, :show %>
<%= %(role=link data-url=#{polymorphic_path([namespace, resource])}) %>
<% end %>
>
<% collection_presenter.attributes_for(resource).each do |attribute| %>
<td class="py-3 px-4 text-sm text-n-slate-12">
<% if authorized_action? resource, :show -%>
<a href="<%= polymorphic_path([namespace, resource]) -%>"
class="hover:underline"
>
<%= render_field attribute %>
</a>
<% end -%>
</td>
<% end %>
<% if existing_action? collection_presenter.resource_name, :edit %>
<td class="py-3 px-4 text-right"><%= link_to(
t("administrate.actions.edit"),
[:edit, namespace, resource],
class: "text-sm text-woot-500 hover:text-woot-600 hover:underline",
) if authorized_action? resource, :edit%></td>
<% end %>
<% if existing_action? collection_presenter.resource_name, :destroy %>
<td class="py-3 px-4 text-right"><%= link_to(
t("administrate.actions.destroy"),
[namespace, resource],
class: "text-sm text-ruby-500 hover:text-ruby-600 hover:underline",
method: :delete,
data: { confirm: t("administrate.actions.confirm") }
) if authorized_action? resource, :destroy %></td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
</tbody>
</table>
</div>
@@ -177,4 +177,65 @@
<symbol id="icon-slack" viewBox="0 0 24 24">
<path fill="currentColor" d="M6.527 14.514A1.973 1.973 0 0 1 4.56 16.48a1.973 1.973 0 0 1-1.968-1.967c0-1.083.885-1.968 1.968-1.968h1.967zm.992 0c0-1.083.885-1.968 1.968-1.968s1.967.885 1.967 1.968v4.927a1.973 1.973 0 0 1-1.967 1.968a1.973 1.973 0 0 1-1.968-1.968zm1.968-7.987A1.973 1.973 0 0 1 7.519 4.56c0-1.083.885-1.967 1.968-1.967s1.967.884 1.967 1.967v1.968zm0 .992c1.083 0 1.967.884 1.967 1.967a1.973 1.973 0 0 1-1.967 1.968H4.56a1.973 1.973 0 0 1-1.968-1.968c0-1.083.885-1.967 1.968-1.967zm7.986 1.967c0-1.083.885-1.967 1.968-1.967s1.968.884 1.968 1.967a1.973 1.973 0 0 1-1.968 1.968h-1.968zm-.991 0a1.973 1.973 0 0 1-1.968 1.968a1.973 1.973 0 0 1-1.968-1.968V4.56c0-1.083.885-1.967 1.968-1.967s1.968.884 1.968 1.967zm-1.968 7.987c1.083 0 1.968.885 1.968 1.968a1.973 1.973 0 0 1-1.968 1.968a1.973 1.973 0 0 1-1.968-1.968v-1.968zm0-.992a1.973 1.973 0 0 1-1.968-1.967c0-1.083.885-1.968 1.968-1.968h4.927c1.083 0 1.968.885 1.968 1.968a1.973 1.973 0 0 1-1.968 1.967z"/>
</symbol>
<!-- Lucide-style icons for sidebar -->
<symbol id="icon-layout-dashboard" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="7" height="9" x="3" y="3" rx="1"/>
<rect width="7" height="5" x="14" y="3" rx="1"/>
<rect width="7" height="9" x="14" y="12" rx="1"/>
<rect width="7" height="5" x="3" y="16" rx="1"/>
</symbol>
<symbol id="icon-building-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z"/>
<path d="M6 12H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2"/>
<path d="M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2"/>
<path d="M10 6h4"/>
<path d="M10 10h4"/>
<path d="M10 14h4"/>
<path d="M10 18h4"/>
</symbol>
<symbol id="icon-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M22 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</symbol>
<symbol id="icon-toy-brick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="18" height="12" x="3" y="8" rx="1"/>
<path d="M10 8V5c0-.6-.4-1-1-1H6a1 1 0 0 0-1 1v3"/>
<path d="M19 8V5c0-.6-.4-1-1-1h-3a1 1 0 0 0-1 1v3"/>
</symbol>
<symbol id="icon-bot" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 8V4H8"/>
<rect width="16" height="12" x="4" y="8" rx="2"/>
<path d="M2 14h2"/>
<path d="M20 14h2"/>
<path d="M15 13v2"/>
<path d="M9 13v2"/>
</symbol>
<symbol id="icon-zap" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>
</symbol>
<symbol id="icon-layers" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/>
<path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/>
<path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/>
</symbol>
<symbol id="icon-heart-pulse" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/>
<path d="M3.22 12H9.5l.5-1 2 4.5 2-7 1.5 3.5h5.27"/>
</symbol>
<symbol id="icon-log-out" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
<polyline points="16 17 21 12 16 7"/>
<line x1="21" x2="9" y1="12" y2="12"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 46 KiB

@@ -1,9 +1,9 @@
<li>
<% is_active = current_page?(url) %>
<% base_class = "flex items-center gap-2 px-2.5 py-2 text-sm rounded-lg transition-colors" %>
<% active_class = is_active ? "text-woot-500 bg-woot-50" : "text-n-slate-11 hover:text-n-slate-12 hover:bg-n-alpha-1" %>
<% base_class = "flex items-center gap-2 px-2 py-1.5 h-8 text-sm font-medium rounded-lg" %>
<% active_class = is_active ? "text-n-blue-text bg-n-alpha-2" : "text-n-slate-11 hover:bg-n-alpha-2" %>
<%= link_to(url, class: "#{base_class} #{active_class}") do %>
<svg width="20" height="20" class="flex-shrink-0"><use xlink:href="#<%= icon %>" /></svg>
<svg width="16" height="16" class="flex-shrink-0"><use xlink:href="#<%= icon %>" /></svg>
<span><%= label %></span>
<% end %>
</li>
@@ -12,16 +12,16 @@ as defined by the routes in the `admin/` namespace
<%
sidebar_icons = {
accounts: 'icon-building-4-line',
users: 'icon-user-follow-line',
platform_apps: 'icon-apps-2-line',
agent_bots: 'icon-robot-line',
accounts: 'icon-building-2',
users: 'icon-users',
platform_apps: 'icon-toy-brick',
agent_bots: 'icon-bot',
}
%>
<div class="border-n-weak border-r w-56 flex-shrink-0 justify-between h-full flex flex-col bg-n-background" role="navigation">
<div class="border-n-weak border-r w-[200px] flex-shrink-0 justify-between h-full flex flex-col bg-n-background" role="navigation">
<div>
<div class="flex items-center mx-4 border-n-weak border-b py-5">
<div class="flex items-center px-4 border-n-weak border-b h-16 mt-px">
<%= link_to image_tag('/brand-assets/logo_thumbnail.svg', alt: 'Chatwoot Admin Dashboard', class: 'h-8'), super_admin_root_url %>
<div class="flex flex-col ml-2.5">
<div class="text-sm font-medium text-n-slate-12">Chatwoot <%= Chatwoot.config[:version] %></div>
@@ -30,7 +30,7 @@ as defined by the routes in the `admin/` namespace
</div>
<ul class="py-4 px-3 space-y-0.5">
<%= render partial: "nav_item", locals: { icon: 'icon-dashboard-line', url: super_admin_root_url, label: 'Dashboard' } %>
<%= render partial: "nav_item", locals: { icon: 'icon-layout-dashboard', url: super_admin_root_url, label: 'Dashboard' } %>
<% Administrate::Namespace.new(namespace).resources.each do |resource| %>
<% next if ["account_users", "access_tokens", "installation_configs", "dashboard", "devise/sessions", "app_configs", "instance_statuses", "settings"].include? resource.resource %>
<%= render partial: "nav_item", locals: {
@@ -46,10 +46,10 @@ as defined by the routes in the `admin/` namespace
</div>
<div class="border-t border-n-weak">
<ul class="py-4 px-3 space-y-0.5">
<%= render partial: "nav_item", locals: { icon: 'icon-mist-fill', url: sidekiq_web_url, label: 'Sidekiq Dashboard' } %>
<%= render partial: "nav_item", locals: { icon: 'icon-health-book-line', url: super_admin_instance_status_url, label: 'Instance Health' } %>
<%= render partial: "nav_item", locals: { icon: 'icon-grid-line', url: '/', label: 'Agent Dashboard' } %>
<%= render partial: "nav_item", locals: { icon: 'icon-logout-circle-r-line', url: super_admin_logout_url, label: 'Logout' } %>
<%= render partial: "nav_item", locals: { icon: 'icon-layers', url: sidekiq_web_url, label: 'Sidekiq Dashboard' } %>
<%= render partial: "nav_item", locals: { icon: 'icon-heart-pulse', url: super_admin_instance_status_url, label: 'Instance Health' } %>
<%= render partial: "nav_item", locals: { icon: 'icon-layout-dashboard', url: '/', label: 'Agent Dashboard' } %>
<%= render partial: "nav_item", locals: { icon: 'icon-log-out', url: super_admin_logout_url, label: 'Logout' } %>
</ul>
</div>
</div>
@@ -1,23 +1,21 @@
<li>
<details class="group" <%= 'open' if open %>>
<summary class="flex items-center gap-2 px-2.5 py-2 text-sm rounded-lg transition-colors cursor-pointer list-none <%= open ? 'text-woot-500 bg-woot-50' : 'text-n-slate-11 hover:text-n-slate-12 hover:bg-n-alpha-1' %>">
<summary class="flex items-center gap-2 px-2 py-1.5 h-8 text-sm font-medium rounded-lg cursor-pointer list-none <%= open ? 'text-n-slate-12' : 'text-n-slate-11 hover:bg-n-alpha-2' %>">
<%= link_to super_admin_settings_url, class: 'flex items-center gap-2 flex-1' do %>
<svg width="20" height="20" class="flex-shrink-0"><use xlink:href="#icon-settings-2-line" /></svg>
<svg width="16" height="16" class="flex-shrink-0"><use xlink:href="#icon-zap" /></svg>
<span>Settings</span>
<% end %>
<svg class="w-4 h-4 transition-transform duration-200 group-open:rotate-180" viewBox="0 0 20 20" fill="currentColor">
<svg class="w-3 h-3 transition-transform duration-200 group-open:rotate-180" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</summary>
<ul class="mt-1 ml-7 space-y-0.5">
<ul class="grid m-0 list-none">
<% settings_pages.each do |_feature_key, attrs| %>
<% url = super_admin_app_config_url(config: attrs['config_key']) %>
<li>
<li class="py-0.5 pl-3 ml-3 relative before:content-[''] before:absolute before:w-0.5 before:h-full before:bg-n-slate-4 before:left-0 before:rounded-full first:before:rounded-t last:before:h-1/2 last:before:top-0">
<% is_active = current_page?(url) %>
<% base_class = "flex items-center px-2.5 py-1.5 text-sm rounded-lg transition-colors" %>
<% active_class = is_active ? "text-woot-500 bg-woot-50" : "text-n-slate-11 hover:text-n-slate-12 hover:bg-n-alpha-1" %>
<%= link_to url, class: "#{base_class} #{active_class}" do %>
<span><%= attrs['name'] %></span>
<%= link_to url, class: "flex h-8 items-center gap-2 px-2 py-1 rounded-lg text-sm hover:bg-n-alpha-2 #{is_active ? 'text-n-blue-text bg-n-alpha-2' : 'text-n-slate-11'}" do %>
<span class="truncate"><%= attrs['name'] %></span>
<% end %>
</li>
<% end %>
@@ -42,7 +42,7 @@ It renders the `_table` partial to display details about the resources.
<section class="flex flex-col w-full h-full">
<header class="sticky top-0 z-10 bg-n-background border-b border-n-weak" role="banner">
<div class="flex items-center justify-between w-full h-20 mx-auto max-w-[60rem]">
<div class="flex items-center justify-between w-full h-16 mx-auto max-w-[60rem]">
<div class="flex items-center gap-3">
<div class="border border-n-weak p-2 rounded-full text-n-slate-11">
<svg width="24" height="24"><use xlink:href="#<%= current_icon %>" /></svg>
@@ -33,7 +33,7 @@ as well as a link to its edit page.
<section class="flex flex-col w-full h-full">
<header class="sticky top-0 z-10 bg-n-background border-b border-n-weak" role="banner">
<div class="flex items-center justify-between w-full h-20 mx-auto max-w-[60rem]">
<div class="flex items-center justify-between w-full h-16 mx-auto max-w-[60rem]">
<div class="flex items-center gap-3">
<div class="border border-n-weak p-2 rounded-full text-n-slate-11">
<svg width="24" height="24"><use xlink:href="#<%= current_icon %>" /></svg>
@@ -7,5 +7,6 @@
usersCount: @users_count,
inboxesCount: @inboxes_count,
conversationsCount: @conversations_count,
chartData: @data
chartData: @data,
chartDays: @chart_days
}) %>
@@ -4,10 +4,10 @@
<section class="flex flex-col w-full h-full">
<header class="sticky top-0 z-10 bg-n-background border-b border-n-weak" role="banner">
<div class="flex items-center justify-between w-full h-20 mx-auto max-w-[60rem]">
<div class="flex items-center justify-between w-full h-16 mx-auto max-w-[60rem]">
<div class="flex items-center gap-3">
<div class="border border-n-weak p-2 rounded-full text-n-slate-11">
<svg width="24" height="24"><use xlink:href="#icon-health-book-line" /></svg>
<svg width="24" height="24"><use xlink:href="#icon-heart-pulse" /></svg>
</div>
<div class="flex flex-col justify-center">
<h1 class="text-base font-medium tracking-tight text-n-slate-12" id="page-title">
@@ -27,17 +27,17 @@
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-n-slate-1">
<thead class="border-b border-n-weak">
<tr>
<th class="text-left py-3 px-6 text-sm font-medium text-n-slate-11">Metric</th>
<th class="text-left py-3 px-6 text-sm font-medium text-n-slate-11">Value</th>
<th class="text-left py-3 pl-8 pr-4 text-sm text-n-slate-11">Metric</th>
<th class="text-left py-3 pl-4 pr-8 text-sm text-n-slate-11">Value</th>
</tr>
</thead>
<tbody class="divide-y divide-n-weak">
<% @metrics.each do |key, value| %>
<tr class="hover:bg-n-alpha-1">
<td class="py-3 px-6 text-sm text-n-slate-12 font-medium"><%= key %></td>
<td class="py-3 px-6 text-sm text-n-slate-11 font-mono"><%= value %></td>
<td class="py-4 pl-8 pr-4 text-sm text-n-slate-12 font-medium"><%= key %></td>
<td class="py-4 pl-4 pr-8 text-sm text-n-slate-11 font-mono"><%= value %></td>
</tr>
<% end %>
</tbody>
+2 -2
View File
@@ -4,9 +4,9 @@
<section class="flex flex-col w-full h-full">
<header class="sticky top-0 z-10 bg-n-background border-b border-n-weak" role="banner">
<div class="flex items-center w-full h-20 mx-auto max-w-[60rem] gap-3">
<div class="flex items-center w-full h-16 mx-auto max-w-[60rem] gap-3">
<div class="border border-n-weak p-2 rounded-full text-n-slate-11">
<svg width="24" height="24"><use xlink:href="#icon-settings-2-line" /></svg>
<svg width="24" height="24"><use xlink:href="#icon-zap" /></svg>
</div>
<div class="flex flex-col justify-center">
<h1 class="text-base font-medium tracking-tight text-n-slate-12" id="page-title">
@@ -18,80 +18,79 @@ to display a collection of resources in an HTML table.
[1]: http://www.rubydoc.info/gems/administrate/Administrate/Page/Collection
%>
<div class="rounded-xl bg-n-solid-2 shadow outline-1 outline outline-n-container overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full" aria-labelledby="<%= table_title %>">
<thead class="bg-n-slate-1">
<tr>
<% collection_presenter.attribute_types.each do |attr_name, attr_type| %>
<th class="text-left py-3 px-4 text-sm font-medium text-n-slate-11"
scope="col"
role="columnheader"
aria-sort="<%= sort_order(collection_presenter.ordered_html_class(attr_name)) %>">
<%= link_to(sanitized_order_params(page, collection_field_name).merge(
collection_presenter.order_params_for(attr_name, key: collection_field_name)
), class: "inline-flex items-center gap-1 hover:text-n-slate-12") do %>
<%= t(
"helpers.label.#{collection_presenter.resource_name}.#{attr_name}",
default: attr_name.to_s,
).titleize %>
<% if collection_presenter.ordered_by?(attr_name) %>
<span class="<%= collection_presenter.ordered_html_class(attr_name) == 'asc' ? '' : 'rotate-180' %>">
<svg class="w-3 h-3" aria-hidden="true">
<use xlink:href="#icon-up-caret" />
</svg>
</span>
<% end %>
<div class="overflow-x-auto">
<table class="w-full" aria-labelledby="<%= table_title %>">
<thead class="border-b border-n-weak">
<tr>
<% collection_presenter.attribute_types.each_with_index do |(attr_name, attr_type), index| %>
<th class="text-left py-3 px-4 text-sm text-n-slate-11 first:!pl-3 last:!pr-3"
scope="col"
role="columnheader"
aria-sort="<%= sort_order(collection_presenter.ordered_html_class(attr_name)) %>">
<%= link_to(sanitized_order_params(page, collection_field_name).merge(
collection_presenter.order_params_for(attr_name, key: collection_field_name)
), class: "inline-flex items-center gap-1 hover:text-n-slate-12") do %>
<%= t(
"helpers.label.#{collection_presenter.resource_name}.#{attr_name}",
default: attr_name.to_s,
).titleize %>
<% if collection_presenter.ordered_by?(attr_name) %>
<span class="<%= collection_presenter.ordered_html_class(attr_name) == 'asc' ? '' : 'rotate-180' %>">
<svg class="w-3 h-3" aria-hidden="true">
<use xlink:href="#icon-up-caret" />
</svg>
</span>
<% end %>
</th>
<% end %>
</th>
<% end %>
<% if existing_action?(collection_presenter.resource_name, :edit) %>
<th scope="col"></th>
<% end %>
<% if existing_action?(collection_presenter.resource_name, :destroy) %>
<th scope="col" class="pr-3"></th>
<% end %>
</tr>
</thead>
<tbody class="divide-y divide-n-weak">
<% resources.each do |resource| %>
<tr class="js-table-row hover:bg-n-alpha-1 transition-colors"
tabindex="0"
<% if existing_action? collection_presenter.resource_name, :show %>
<%= %(role=link data-url=#{polymorphic_path([namespace, resource.becomes(User)])}) %>
<% end %>
>
<% collection_presenter.attributes_for(resource).each_with_index do |attribute, index| %>
<td class="py-4 px-4 text-sm text-n-slate-12 first:!pl-3 last:!pr-3">
<% if authorized_action? resource, :show -%>
<a href="<%= polymorphic_path([namespace, resource.becomes(User)]) -%>"
class="hover:underline"
>
<%= render_field attribute %>
</a>
<% end -%>
</td>
<% end %>
<% [existing_action?(collection_presenter.resource_name, :edit),
existing_action?(collection_presenter.resource_name, :destroy)].count(true).times do %>
<th scope="col" class="py-3 px-4"></th>
<% if existing_action?(collection_presenter.resource_name, :edit) %>
<td class="py-4 px-4 text-right"><%= link_to(
t("administrate.actions.edit"),
[:edit, namespace, resource.becomes(User)],
class: "text-sm font-medium text-n-brand hover:underline",
) if authorized_action? resource, :edit%></td>
<% end %>
<% if existing_action?(collection_presenter.resource_name, :destroy) %>
<td class="py-4 pl-4 !pr-3 text-right"><%= link_to(
t("administrate.actions.destroy"),
[namespace, collection_presenter.resource_name == 'account_user' ? resource : resource.becomes(User)],
class: "text-sm font-medium text-n-ruby-11 hover:underline",
method: :delete,
data: { confirm: t("administrate.actions.confirm") }
) if authorized_action? resource, :destroy %></td>
<% end %>
</tr>
</thead>
<tbody class="divide-y divide-n-weak">
<% resources.each do |resource| %>
<tr class="js-table-row hover:bg-n-alpha-1 transition-colors"
tabindex="0"
<% if existing_action? collection_presenter.resource_name, :show %>
<%= %(role=link data-url=#{polymorphic_path([namespace, resource.becomes(User)])}) %>
<% end %>
>
<% collection_presenter.attributes_for(resource).each do |attribute| %>
<td class="py-3 px-4 text-sm text-n-slate-12">
<% if authorized_action? resource, :show -%>
<a href="<%= polymorphic_path([namespace, resource.becomes(User)]) -%>"
class="hover:underline"
>
<%= render_field attribute %>
</a>
<% end -%>
</td>
<% end %>
<% if existing_action? collection_presenter.resource_name, :edit %>
<td class="py-3 px-4 text-right"><%= link_to(
t("administrate.actions.edit"),
[:edit, namespace, resource.becomes(User)],
class: "text-sm text-woot-500 hover:text-woot-600 hover:underline",
) if authorized_action? resource, :edit%></td>
<% end %>
<% if existing_action? collection_presenter.resource_name, :destroy %>
<td class="py-3 px-4 text-right"><%= link_to(
t("administrate.actions.destroy"),
[namespace, collection_presenter.resource_name == 'account_user' ? resource : resource.becomes(User)],
class: "text-sm text-ruby-500 hover:text-ruby-600 hover:underline",
method: :delete,
data: { confirm: t("administrate.actions.confirm") }
) if authorized_action? resource, :destroy %></td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
</tbody>
</table>
</div>
+14 -14
View File
@@ -20,10 +20,10 @@ as well as a link to its edit page.
<section class="flex flex-col w-full h-full">
<header class="sticky top-0 z-10 bg-n-background border-b border-n-weak" role="banner">
<div class="flex items-center justify-between w-full h-20 mx-auto max-w-[60rem]">
<div class="flex items-center justify-between w-full h-16 mx-auto max-w-[60rem]">
<div class="flex items-center gap-3">
<div class="border border-n-weak p-2 rounded-full text-n-slate-11">
<svg width="24" height="24"><use xlink:href="#icon-user-follow-line" /></svg>
<svg width="24" height="24"><use xlink:href="#icon-users" /></svg>
</div>
<div class="flex flex-col justify-center">
<h1 class="text-base font-medium tracking-tight text-n-slate-12">
@@ -85,38 +85,38 @@ as well as a link to its edit page.
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-n-slate-1">
<thead class="border-b border-n-weak">
<tr>
<th class="text-left py-3 px-6 text-sm font-medium text-n-slate-11">Account</th>
<th class="text-left py-3 px-6 text-sm font-medium text-n-slate-11">User</th>
<th class="text-left py-3 px-6 text-sm font-medium text-n-slate-11">Inviter</th>
<th class="text-left py-3 px-6 text-sm font-medium text-n-slate-11">Role</th>
<th class="text-left py-3 px-6 text-sm font-medium text-n-slate-11"></th>
<th class="text-left py-3 !pl-6 pr-4 text-sm text-n-slate-11">Account</th>
<th class="text-left py-3 px-4 text-sm text-n-slate-11">User</th>
<th class="text-left py-3 px-4 text-sm text-n-slate-11">Inviter</th>
<th class="text-left py-3 px-4 text-sm text-n-slate-11">Role</th>
<th class="text-left py-3 pl-4 !pr-6 text-sm text-n-slate-11"></th>
</tr>
</thead>
<tbody class="divide-y divide-n-weak">
<% page.resource.account_users.each do |account_user| %>
<tr class="hover:bg-n-alpha-1">
<td class="py-3 px-6 text-sm text-n-slate-12">
<td class="py-4 !pl-6 pr-4 text-sm text-n-slate-12">
<%= link_to "##{account_user.account_id} #{account_user.account.name}", super_admin_account_path(account_user.account), class: "text-n-slate-12 hover:underline" %>
</td>
<td class="py-3 px-6 text-sm text-n-slate-12">
<td class="py-4 px-4 text-sm text-n-slate-12">
<%= link_to "##{account_user.user_id} #{account_user.user.name}", super_admin_user_path(account_user.user), class: "text-n-slate-12 hover:underline" %>
</td>
<td class="py-3 px-6 text-sm text-n-slate-11">
<td class="py-4 px-4 text-sm text-n-slate-11">
<% if account_user.inviter.present? %>
<%= link_to account_user.inviter.name, super_admin_user_path(account_user.inviter), class: "text-n-slate-12 hover:underline" %>
<% else %>
<span class="text-n-slate-10">---</span>
<% end %>
</td>
<td class="py-3 px-6">
<td class="py-4 px-4">
<span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium bg-n-alpha-2 text-n-slate-12 capitalize">
<%= account_user.role %>
</span>
</td>
<td class="py-3 px-6 text-right">
<%= link_to "Destroy", super_admin_account_user_path(account_user), method: :delete, data: { confirm: "Are you sure?" }, class: "text-sm text-ruby-500 hover:text-ruby-600 hover:underline" %>
<td class="py-4 pl-4 !pr-6 text-right">
<%= link_to "Destroy", super_admin_account_user_path(account_user), method: :delete, data: { confirm: "Are you sure?" }, class: "text-sm font-medium text-n-ruby-11 hover:underline" %>
</td>
</tr>
<% end %>