SuperAdmin

This commit is contained in:
Pranav
2025-12-18 12:59:45 -08:00
parent 8019e7c636
commit 05f9c3053d
31 changed files with 1100 additions and 719 deletions
@@ -13,28 +13,30 @@ defineProps({
<template>
<header
class="flex items-center px-8 py-4 bg-white border-b border-slate-100"
class="sticky top-0 z-10 bg-n-background border-b border-n-weak"
role="banner"
>
<a :href="responseSourcePath" class="text-woot-500 hover:underline mr-4">
{{ 'Back' }}
</a>
<div
class="border border-solid border-slate-100 text-slate-700 mr-4 p-2 rounded-full"
>
<svg width="24" height="24"><use xlink:href="#icon-mist-fill" /></svg>
</div>
<div class="flex flex-col h-14 justify-center">
<h1 id="page-title" class="text-base font-medium text-slate-900">
{{ 'Robin AI playground' }}
</h1>
<p class="text-sm text-slate-600">
{{ 'Chat with the source' }}
<span class="font-medium">
{{ responseSourceName }}
</span>
{{ 'and evaluate its efficiency.' }}
</p>
<div class="flex items-center h-16 mx-auto max-w-[60rem]">
<a :href="responseSourcePath" class="text-woot-500 hover:underline mr-4">
Back
</a>
<div
class="border border-solid border-n-weak text-n-slate-11 mr-4 p-2 rounded-full"
>
<svg width="24" height="24"><use xlink:href="#icon-mist-fill" /></svg>
</div>
<div class="flex flex-col justify-center">
<h1 id="page-title" class="text-lg font-medium text-n-slate-12">
Robin AI playground
</h1>
<p class="text-sm text-n-slate-11">
Chat with the source
<span class="font-medium text-n-slate-12">
{{ responseSourceName }}
</span>
and evaluate it's efficiency.
</p>
</div>
</div>
</header>
</template>
@@ -38,38 +38,77 @@ const { accountsCount, usersCount, inboxesCount, conversationsCount } =
</script>
<template>
<div class="w-full h-full">
<header class="main-content__header" role="banner">
<h1 id="page-title" class="main-content__page-title">
{{ 'Admin Dashboard' }}
</h1>
</header>
<section class="main-content__body main-content__body--flush">
<div class="report--list">
<div class="report-card">
<div class="metric">{{ accountsCount }}</div>
<div>{{ 'Accounts' }}</div>
</div>
<div class="report-card">
<div class="metric">{{ usersCount }}</div>
<div>{{ 'Users' }}</div>
</div>
<div class="report-card">
<div class="metric">{{ inboxesCount }}</div>
<div>{{ 'Inboxes' }}</div>
</div>
<div class="report-card">
<div class="metric">{{ conversationsCount }}</div>
<div>{{ 'Conversations' }}</div>
<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 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-dashboard-line" />
</svg>
</div>
<div class="flex flex-col justify-center">
<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">
Accounts, users, and conversation metrics
</p>
</div>
</div>
</div>
</section>
<!-- eslint-disable vue/no-static-inline-styles -->
<BarChart
class="p-8 w-full"
:collection="chartData"
style="max-height: 500px"
/>
</div>
</header>
<main class="flex-1 px-6 overflow-y-auto">
<div class="w-full mx-auto max-w-[60rem] py-4">
<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"
>
<div class="text-3xl font-semibold text-n-slate-12">
{{ accountsCount }}
</div>
<div class="text-sm text-n-slate-11">Accounts</div>
</div>
<div
class="p-4 rounded-xl bg-n-solid-2 outline outline-1 outline-n-container shadow-sm"
>
<div class="text-3xl font-semibold text-n-slate-12">
{{ usersCount }}
</div>
<div class="text-sm text-n-slate-11">Users</div>
</div>
<div
class="p-4 rounded-xl bg-n-solid-2 outline outline-1 outline-n-container shadow-sm"
>
<div class="text-3xl font-semibold text-n-slate-12">
{{ inboxesCount }}
</div>
<div class="text-sm text-n-slate-11">Inboxes</div>
</div>
<div
class="p-4 rounded-xl bg-n-solid-2 outline outline-1 outline-n-container shadow-sm"
>
<div class="text-3xl font-semibold text-n-slate-12">
{{ conversationsCount }}
</div>
<div class="text-sm text-n-slate-11">Conversations</div>
</div>
</div>
<div
class="p-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>
</div>
</main>
</section>
</template>
@@ -99,37 +99,41 @@ export default {
</script>
<template>
<section class="flex flex-col w-full h-full bg-slate-25">
<section class="flex flex-col w-full h-full bg-n-background">
<PlaygroundHeader
:response-source-name="componentData.responseSourceName"
:response-source-path="componentData.responseSourcePath"
/>
<div class="flex-1 px-8 py-4 overflow-auto">
<div
v-for="message in messages"
:id="`message-${message.id}`"
:key="message.id"
>
<UserMessage
v-if="message.type === 'User'"
:message="formatMessage(message.content)"
/>
<BotMessage v-else :message="formatMessage(message.content)" />
<div class="flex-1 px-6 py-4 overflow-auto">
<div class="w-full mx-auto max-w-[60rem]">
<div
v-for="message in messages"
:id="`message-${message.id}`"
:key="message.id"
>
<UserMessage
v-if="message.type === 'User'"
:message="formatMessage(message.content)"
/>
<BotMessage v-else :message="formatMessage(message.content)" />
</div>
<TypingIndicator v-if="isWaiting" />
</div>
<TypingIndicator v-if="isWaiting" />
</div>
<div class="w-full px-8 py-6">
<textarea
ref="messageInput"
v-model="messageContent"
:rows="4"
class="resize-none block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border !outline-2 border-slate-100 focus:ring-woot-500 focus:border-woot-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-woot-500 dark:focus:border-woot-500"
placeholder="Type a message... [CMD/CTRL + Enter to send]"
autofocus
autocomplete="off"
@keydown.meta.enter="onMessageSend"
@keydown.ctrl.enter="onMessageSend"
/>
<div class="sticky bottom-0 z-10 px-6 py-6 bg-n-background">
<div class="w-full mx-auto max-w-[60rem]">
<textarea
ref="messageInput"
v-model="messageContent"
:rows="4"
class="resize-none block p-3 w-full text-sm text-n-slate-12 bg-n-solid-2 rounded-xl border border-n-weak focus:ring-2 focus:ring-woot-500 focus:border-woot-500 outline-none"
placeholder="Type a message... [CMD/CTRL + Enter to send]"
autofocus
autocomplete="off"
@keydown.meta.enter="onMessageSend"
@keydown.ctrl.enter="onMessageSend"
/>
</div>
</div>
</section>
</template>
+4 -5
View File
@@ -16,11 +16,10 @@ that displays all possible records to associate with.
[1]: http://www.rubydoc.info/gems/administrate/Administrate/Field/BelongsTo
%>
<div class="field-unit__label">
<%= f.label field.permitted_attribute %>
</div>
<div class="field-unit__field">
<div class="flex flex-col gap-1">
<%= f.label field.permitted_attribute, class: "text-sm font-medium text-n-slate-11" %>
<%= f.select(field.permitted_attribute,
options_for_select(field.associated_resource_options, field.selected_option),
include_blank: field.include_blank_option) %>
{ include_blank: field.include_blank_option },
{ class: "h-9 px-3 min-w-[200px] 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,10 @@
<div class="flex flex-col gap-1 field-unit--belongs-to-search">
<%= f.label field.permitted_attribute, class: "text-sm font-medium text-n-slate-11" %>
<%= f.select(field.permitted_attribute,
nil,
{ include_blank: true },
class: "h-9 px-3 min-w-[200px] 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 selectize-input",
'data-url': polymorphic_url([namespace, field.associated_class], format: :json)) do %>
<%= options_for_select(field.associated_resource_options, field.selected_option) %>
<% end %>
</div>
+7
View File
@@ -0,0 +1,7 @@
<div class="flex items-center gap-2">
<%= f.check_box(
field.attribute,
class: "h-4 w-4 text-woot-500 bg-n-solid-2 border border-n-weak rounded focus:ring-2 focus:ring-woot-500 focus:ring-offset-0"
) %>
<%= f.label field.attribute, class: "text-sm font-medium text-n-slate-12" %>
</div>
+8
View File
@@ -0,0 +1,8 @@
<div class="flex flex-col gap-1">
<%= f.label field.attribute, class: "text-sm font-medium text-n-slate-11" %>
<%= f.email_field(
field.attribute,
value: field.data,
class: "h-9 px-3 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>
+8
View File
@@ -0,0 +1,8 @@
<div class="flex flex-col gap-1">
<%= f.label field.attribute, class: "text-sm font-medium text-n-slate-11" %>
<%= f.number_field(
field.attribute,
value: field.data,
class: "h-9 px-3 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>
+7
View File
@@ -0,0 +1,7 @@
<div class="flex flex-col gap-1">
<%= f.label field.attribute, class: "text-sm font-medium text-n-slate-11" %>
<%= f.password_field(
field.attribute,
class: "h-9 px-3 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>
+9
View File
@@ -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: field.include_blank_option },
{ 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>
+8
View File
@@ -0,0 +1,8 @@
<div class="flex flex-col gap-1">
<%= f.label field.attribute, class: "text-sm font-medium text-n-slate-11" %>
<%= f.text_field(
field.attribute,
value: field.data,
class: "h-9 px-3 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>
+9
View File
@@ -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.text_area(
field.attribute,
value: field.data,
rows: 4,
class: "px-3 py-2 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 resize-y"
) %>
</div>
@@ -30,7 +30,7 @@ By default, it renders:
<%= render "icons" %>
<div class="flex w-full h-full">
<%= render "navigation" -%>
<main class="w-full overflow-auto" role="main">
<main class="w-full overflow-auto bg-n-background" role="main">
<%= render "flashes" -%>
<%= yield %>
</main>
@@ -1,9 +1,7 @@
<section class="main-content__body">
<hr/>
<%= form_for([:reset_cache, namespace, page.resource], method: :post, html: { class: "form" }) do |f| %>
<div class="form-actions">
<p class="pb-3">This will clear the IndexedDB cache keys from redis. <br>The next load will fetch the data from backend.</p>
<%= f.submit 'Reset Frontend Cache' %>
</div>
<div class="px-6 py-4">
<h3 class="text-sm font-medium text-n-slate-12 mb-2">Reset Frontend Cache</h3>
<p class="text-xs text-n-slate-11 mb-3">This will clear the IndexedDB cache keys from redis. The next load will fetch the data from backend.</p>
<%= form_for([:reset_cache, namespace, page.resource], method: :post) do |f| %>
<%= f.submit 'Reset Frontend Cache', class: "inline-flex items-center justify-center h-8 px-3 text-sm font-medium text-n-slate-12 bg-n-alpha-2 rounded-lg hover:bg-n-alpha-3 transition-colors cursor-pointer" %>
<% end %>
</section>
</div>
@@ -1,15 +1,10 @@
<% if ENV.fetch('ENABLE_ACCOUNT_SEEDING', !Rails.env.production?) %>
<section class="main-content__body">
<hr/>
<%= form_for([:seed, namespace, page.resource], method: :post, html: { class: "form" }) do |f| %>
<div class="form-actions">
<div class="pb-3">
<p>Click the button to generate seed data into this account for demos.</p>
<p class="text-color-red">Note: This will clear all the existing data in this account.</p>
</div>
<%= f.submit 'Generate Seed Data' %>
</div>
<% if ENV.fetch('ENABLE_ACCOUNT_SEEDING', !Rails.env.production?) %>
<div class="px-6 py-4">
<h3 class="text-sm font-medium text-n-slate-12 mb-2">Generate Seed Data</h3>
<p class="text-xs text-n-slate-11 mb-1">Click the button to generate seed data into this account for demos.</p>
<p class="text-xs text-ruby-500 mb-3">Note: This will clear all the existing data in this account.</p>
<%= form_for([:seed, namespace, page.resource], method: :post) do |f| %>
<%= f.submit 'Generate Seed Data', class: "inline-flex items-center justify-center h-8 px-3 text-sm font-medium text-ruby-500 bg-ruby-100 rounded-lg hover:bg-ruby-200 transition-colors cursor-pointer" %>
<% end %>
</section>
</div>
<% end %>
+154 -69
View File
@@ -15,84 +15,169 @@ as well as a link to its edit page.
[1]: http://www.rubydoc.info/gems/administrate/Administrate/Page/Show %>
<% content_for(:title) do
t("administrate.actions.show_resource", name: page.page_title)
end %>
<% content_for(:title) do
t("administrate.actions.show_resource", name: page.page_title)
end %>
<header class="main-content__header" role="banner">
<h1 class="main-content__page-title">
<%= content_for(:title) %>
</h1>
<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 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>
</div>
<div class="flex flex-col justify-center">
<h1 class="text-base font-medium tracking-tight text-n-slate-12">
<%= content_for(:title) %>
</h1>
<p class="text-xs text-n-slate-11 m-0">Billing, features, and user management</p>
</div>
</div>
<div>
<%= if accessible_action?(page.resource, :edit)
link_to("Edit", [:edit, namespace, page.resource], class: "button")
end %>
<div class="flex items-center gap-2">
<% if accessible_action?(page.resource, :edit) %>
<%= link_to("Edit", [:edit, namespace, page.resource], class: "inline-flex items-center justify-center h-8 px-3 text-sm font-medium text-white bg-woot-500 rounded-lg hover:bg-woot-600 transition-colors") %>
<% end %>
</div>
</div>
</header>
<section class="main-content__body">
<div class="pr-16">
<% page.attributes.each do |title, attributes| %>
<% if title.present? && title == 'all_features' %>
<% regular_features, premium_features =
attributes.partition do |attr|
!SuperAdmin::AccountFeaturesHelper.account_premium_features.include?(
attr.data.keys.first,
)
end %>
<div class="space-y-4">
<% regular_features.each do |attribute| %>
<div class="attribute-label" id="<%= attribute.name %>">
<%= t(
"helpers.label.#{resource_name}.#{attribute.name}",
default: page.resource.class.human_attribute_name(attribute.name),
) %>
</div>
<div class="attribute-data attribute-data--<%=attribute.html_class%>"><%= render_field attribute, page: page %></div>
<% end %>
</div>
<hr class="my-8 border-n-weak">
<div class="space-y-4">
<% premium_features.each do |attribute| %>
<div class="attribute-label" id="<%= attribute.name %>">
<%= t(
"helpers.label.#{resource_name}.#{attribute.name}",
default: page.resource.class.human_attribute_name(attribute.name),
) %>
</div>
<div class="attribute-data attribute-data--<%=attribute.html_class%>"><%= render_field attribute, page: page %></div>
<% end %>
</div>
<% else %>
<fieldset class="<%= "field-unit--nested" if title.present? %>">
<% if title.present? %>
<legend><%= t "helpers.label.#{page.resource_name}.#{title}", default: title %></legend>
<% end %>
<main class="flex-1 px-6 overflow-y-auto">
<div class="w-full mx-auto max-w-[60rem] py-6 space-y-6">
<%# Basic Information Card %>
<div class="rounded-xl bg-n-solid-2 shadow outline-1 outline outline-n-container overflow-hidden">
<div class="px-6 py-4 border-b border-n-weak">
<h2 class="text-base font-medium text-n-slate-12">Account Information</h2>
</div>
<div class="divide-y divide-n-weak">
<% page.attributes.each do |title, attributes| %>
<% next if title.present? %>
<% attributes.each do |attribute| %>
<div class="attribute-label" id="<%= attribute.name %>">
<%= t(
"helpers.label.#{resource_name}.#{attribute.name}",
default: page.resource.class.human_attribute_name(attribute.name),
) %>
<% next if ['account_users', 'custom_attributes', 'all_features'].include?(attribute.name.to_s) %>
<div class="grid grid-cols-[180px,1fr] items-center min-h-12 px-6">
<span class="text-sm text-n-slate-11">
<%= t(
"helpers.label.#{resource_name}.#{attribute.name}",
default: page.resource.class.human_attribute_name(attribute.name),
) %>
</span>
<span class="text-sm text-n-slate-12 py-3">
<%= render_field attribute, page: page %>
</span>
</div>
<div class="attribute-data attribute-data--<%=attribute.html_class%>"><%= render_field attribute, page: page %></div>
<% end %>
</fieldset>
<% end %>
</div>
</div>
<%# Account Users Card %>
<% page.attributes.each do |title, attributes| %>
<% next if title.present? %>
<% attributes.each do |attribute| %>
<% if attribute.name.to_s == 'account_users' && page.resource.account_users.any? %>
<div class="rounded-xl bg-n-solid-2 shadow outline-1 outline outline-n-container overflow-hidden">
<div class="px-6 py-4 border-b border-n-weak">
<h2 class="text-base font-medium text-n-slate-12">Account Users</h2>
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-n-slate-1">
<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>
</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">
<%= 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">
<%= 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">
<% 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">
<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>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% end %>
<% end %>
<% end %>
<%# Custom Attributes Card %>
<% if page.resource.custom_attributes.present? %>
<div class="rounded-xl bg-n-solid-2 shadow outline-1 outline outline-n-container overflow-hidden">
<div class="px-6 py-4 border-b border-n-weak">
<h2 class="text-base font-medium text-n-slate-12">Custom Attributes</h2>
</div>
<div class="divide-y divide-n-weak">
<% page.resource.custom_attributes.each do |key, value| %>
<div class="grid grid-cols-[180px,1fr] items-start min-h-12 px-6">
<span class="text-sm text-n-slate-11 py-3">
<%= key.to_s.titleize %>
</span>
<span class="text-sm text-n-slate-12 py-3 break-all font-mono">
<%= value %>
</span>
</div>
<% end %>
</div>
</div>
<% end %>
<%# Features Section %>
<% page.attributes.each do |title, attributes| %>
<% next if title.present? %>
<% attributes.each do |attribute| %>
<% if attribute.name.to_s == 'all_features' %>
<div class="rounded-xl bg-n-solid-2 shadow outline-1 outline outline-n-container overflow-hidden">
<div class="px-6 py-4 border-b border-n-weak">
<h2 class="text-base font-medium text-n-slate-12">Features</h2>
</div>
<div class="p-6">
<%= render_field attribute, page: page %>
</div>
</div>
<% end %>
<% end %>
<% end %>
<%# Actions Card %>
<div class="rounded-xl bg-n-solid-2 shadow outline-1 outline outline-n-container overflow-hidden">
<div class="px-6 py-4 border-b border-n-weak">
<h2 class="text-base font-medium text-n-slate-12">Actions</h2>
</div>
<div class="divide-y divide-n-weak">
<div class="px-6 py-4">
<h3 class="text-sm font-medium text-n-slate-12 mb-3">Add User to Account</h3>
<%= render 'super_admin/shared/account_user_form', page: page, namespace: namespace, resource_type: 'account' %>
</div>
<%= render partial: "seed_data", locals: { page: page } %>
<%= render partial: "reset_cache", locals: { page: page } %>
</div>
</div>
</div>
</section>
<%= render 'super_admin/shared/account_user_form', page: page, namespace: namespace, resource_type: 'account' %>
<%= render partial: "seed_data", locals: { page: page } %>
<%= render partial: "reset_cache", locals: { page: page } %>
</main>
</section>
+76 -62
View File
@@ -2,78 +2,92 @@
Configure Settings - <%= @config.titleize %>
<% end %>
<header class="main-content__header" role="banner">
<h1 class="main-content__page-title" id="page-title">
<%= content_for(:title) %>
</h1>
</header>
<style>
.eye-icon.eye-hide path {
d: path('M3 3l18 18M10.5 10.677a2 2 0 002.823 2.823M7.362 7.561C5.68 8.74 4.279 10.42 3 12c1.889 2.991 5.282 6 9 6 1.55 0 3.043-.523 4.395-1.35M12 6c4.008 0 6.701 3.009 9 6a15.66 15.66 0 01-1.078 1.5');
}
</style>
<section class="main-content__body">
<%= form_with url: super_admin_app_config_url(config: @config) , method: :post do |form| %>
<% @allowed_configs.each do |key| %>
<div class="flex mb-8">
<div class="field-unit__label">
<%= form.label "app_config[#{key}]", @installation_configs[key]&.dig('display_title') || key %>
<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 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>
</div>
<div class="-mt-2 field-unit__field ">
<% if @installation_configs[key]&.dig('type') == 'boolean' %>
<%= form.select "app_config[#{key}]",
[["True", true], ["False", false]],
{ selected: ActiveModel::Type::Boolean.new.cast(@app_config[key]) },
class: "mt-2 border border-slate-100 p-1 rounded-md"
%>
<% elsif @installation_configs[key]&.dig('type') == 'code' %>
<%= form.text_area "app_config[#{key}]",
value: @app_config[key],
rows: 12,
wrap: 'off',
class: "mt-2 border font-mono text-xs border-slate-100 p-1 rounded-md overflow-scroll"
%>
<% elsif @installation_configs[key]&.dig('type') == 'secret' %>
<div class="relative">
<%= form.password_field "app_config[#{key}]",
id: "app_config_#{key}",
value: @app_config[key],
class: "mt-2 border border-slate-100 p-1.5 pr-8 rounded-md w-full"
%>
<button
type="button"
class="absolute reset-base !bg-white top-1/2 !outline-0 !text-n-slate-11 -translate-y-1/2 right-2 p-1 hover:!bg-n-slate-5 rounded-sm toggle-password"
data-target="app_config_<%= key %>"
>
<svg class="eye-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 5C5.63636 5 2 12 2 12C2 12 5.63636 19 12 19C18.3636 19 22 12 22 12C22 12 18.3636 5 12 5Z"/>
<path d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z"/>
</svg>
</button>
</div>
<% elsif @installation_configs[key]&.dig('type') == 'select' && @installation_configs[key]&.dig('options').present? %>
<%= form.select "app_config[#{key}]",
@installation_configs[key]['options'].map { |val, label| [label, val] },
{ selected: @app_config[key] },
class: "mt-2 border border-slate-100 p-1 rounded-md"
%>
<% else %>
<%= form.text_field "app_config[#{key}]", value: @app_config[key] %>
<% end %>
<%if @installation_configs[key]&.dig('description').present? %>
<p class="pt-2 text-xs italic text-slate-400">
<%= @installation_configs[key]&.dig('description') %>
</p>
<% end %>
<div class="flex flex-col justify-center">
<h1 class="text-base font-medium tracking-tight text-n-slate-12" id="page-title">
<%= content_for(:title) %>
</h1>
<p class="text-xs text-n-slate-11 m-0">Environment and instance configuration</p>
</div>
</div>
<% end %>
<div class="form-actions">
<%= form.submit "Submit" %>
</div>
<% end %>
</header>
<main class="flex-1 px-6 overflow-y-auto">
<div class="w-full mx-auto max-w-[60rem] py-4">
<%= form_with url: super_admin_app_config_url(config: @config), method: :post do |form| %>
<% @allowed_configs.each do |key| %>
<div class="flex mb-8">
<div class="field-unit__label">
<%= form.label "app_config[#{key}]", @installation_configs[key]&.dig('display_title') || key %>
</div>
<div class="-mt-2 field-unit__field">
<% if @installation_configs[key]&.dig('type') == 'boolean' %>
<%= form.select "app_config[#{key}]",
[["True", true], ["False", false]],
{ selected: ActiveModel::Type::Boolean.new.cast(@app_config[key]) },
class: "mt-2 border border-n-weak p-1 rounded-md bg-n-solid-2"
%>
<% elsif @installation_configs[key]&.dig('type') == 'code' %>
<%= form.text_area "app_config[#{key}]",
value: @app_config[key],
rows: 12,
wrap: 'off',
class: "mt-2 border font-mono text-xs border-n-weak p-1 rounded-md overflow-scroll bg-n-solid-2"
%>
<% elsif @installation_configs[key]&.dig('type') == 'secret' %>
<div class="relative">
<%= form.password_field "app_config[#{key}]",
id: "app_config_#{key}",
value: @app_config[key],
class: "mt-2 border border-n-weak p-1.5 pr-8 rounded-md w-full bg-n-solid-2"
%>
<button
type="button"
class="absolute reset-base !bg-transparent top-1/2 !outline-0 !text-n-slate-11 -translate-y-1/2 right-2 p-1 hover:!bg-n-alpha-2 rounded-sm toggle-password"
data-target="app_config_<%= key %>"
>
<svg class="eye-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 5C5.63636 5 2 12 2 12C2 12 5.63636 19 12 19C18.3636 19 22 12 22 12C22 12 18.3636 5 12 5Z"/>
<path d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z"/>
</svg>
</button>
</div>
<% elsif @installation_configs[key]&.dig('type') == 'select' && @installation_configs[key]&.dig('options').present? %>
<%= form.select "app_config[#{key}]",
@installation_configs[key]['options'].map { |val, label| [label, val] },
{ selected: @app_config[key] },
class: "mt-2 border border-n-weak p-1 rounded-md bg-n-solid-2"
%>
<% else %>
<%= form.text_field "app_config[#{key}]", value: @app_config[key], class: "border border-n-weak p-1.5 rounded-md bg-n-solid-2" %>
<% end %>
<% if @installation_configs[key]&.dig('description').present? %>
<p class="pt-2 text-xs italic text-n-slate-11">
<%= @installation_configs[key]&.dig('description') %>
</p>
<% end %>
</div>
</div>
<% end %>
<div class="pt-4">
<%= form.submit "Submit", class: "inline-flex items-center justify-center h-8 px-4 text-sm font-medium text-white bg-woot-500 rounded-lg hover:bg-woot-600 transition-colors cursor-pointer" %>
</div>
<% end %>
</div>
</main>
</section>
<% content_for :javascript do %>
@@ -18,78 +18,80 @@ to display a collection of resources in an HTML table.
[1]: http://www.rubydoc.info/gems/administrate/Administrate/Page/Collection
%>
<table aria-labelledby="<%= table_title %>">
<thead>
<tr>
<% collection_presenter.attribute_types.each do |attr_name, attr_type| %>
<th class="cell-label
cell-label--<%= attr_type.html_class %>
cell-label--<%= collection_presenter.ordered_html_class(attr_name) %>"
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)
)) 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="cell-label__sort-indicator cell-label__sort-indicator--<%= collection_presenter.ordered_html_class(attr_name) %>">
<svg aria-hidden="true">
<use xlink:href="#icon-up-caret" />
</svg>
</span>
<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 %>
<% end %>
</th>
<% 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>
<% 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 %>
<% end %>
</th>
<% end %>
<% [existing_action?(collection_presenter.resource_name, :edit),
existing_action?(collection_presenter.resource_name, :destroy)].count(true).times do %>
<th scope="col"></th>
<% end %>
</tr>
</thead>
<tbody>
<% resources.each do |resource| %>
<tr class="js-table-row"
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="cell-data cell-data--<%= attribute.html_class %>">
<% if authorized_action? resource, :show -%>
<a href="<%= polymorphic_path([namespace, resource]) -%>"
class="action-show"
>
<%= 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, :edit %>
<td><%= link_to(
t("administrate.actions.edit"),
[:edit, namespace, resource],
class: "action-edit",
) if authorized_action? resource, :edit%></td>
<% 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 %>
<% if existing_action? collection_presenter.resource_name, :destroy %>
<td><%= link_to(
t("administrate.actions.destroy"),
[namespace, resource],
class: "text-color-red",
method: :delete,
data: { confirm: t("administrate.actions.confirm") }
) if authorized_action? resource, :destroy %></td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
</tbody>
</table>
</div>
</div>
@@ -28,27 +28,23 @@ for a collection of resources.
%>
<% if dashboard_class.const_defined?(:COLLECTION_FILTERS) && !dashboard_class::COLLECTION_FILTERS.empty? %>
<div class="flex items-center bg-gray-100 border-0 rounded-md shadow-none relative w-[260px]">
<div class="flex items-center h-10 px-2 w-full">
<div class="flex items-center justify-center flex-shrink-0 mr-2 text-gray-500" title="Filter by">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
</svg>
</div>
<div class="flex-1 h-full min-w-0 relative">
<select id="filter-select" class="appearance-none bg-transparent border-0 text-gray-700 cursor-pointer text-sm h-full overflow-hidden truncate whitespace-nowrap w-full pr-7 pl-0 py-2 focus:outline-none bg-[url('data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%2712%27 height=%2712%27 viewBox=%270 0 12 12%27%3E%3Cpath fill=%27%23293f54%27 d=%27M6 9L1 4h10z%27/%3E%3C/svg%3E')] bg-[right_0.25rem_center] bg-no-repeat bg-[length:0.75rem]" onchange="applyFilter(this.value)">
<option value="">All records</option>
<% dashboard_class::COLLECTION_FILTERS.each do |filter_name, _| %>
<option value="<%= filter_name %>" <%= 'selected' if filter_name.to_s == current_filter %>>
<%= filter_name.to_s.titleize %>
</option>
<% end %>
</select>
<% if current_filter %>
<a href="?" class="flex items-center justify-center rounded-full text-gray-500 text-xl font-bold h-[18px] w-[18px] leading-none absolute right-5 top-1/2 -translate-y-1/2 no-underline z-2 hover:text-gray-900" title="Clear filter">×</a>
<% end %>
</div>
<div class="relative flex items-center">
<div class="absolute left-3 text-n-slate-10">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
</svg>
</div>
<select id="filter-select" class="h-9 pl-9 pr-8 text-sm text-n-slate-12 bg-n-solid-2 border border-n-weak rounded-lg cursor-pointer appearance-none focus:ring-2 focus:ring-woot-500 focus:border-woot-500 outline-none bg-[url('data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%2712%27 height=%2712%27 viewBox=%270 0 12 12%27%3E%3Cpath fill=%27%236b7280%27 d=%27M6 9L1 4h10z%27/%3E%3C/svg%3E')] bg-[right_0.5rem_center] bg-no-repeat bg-[length:0.75rem]" onchange="applyFilter(this.value)">
<option value="">All records</option>
<% dashboard_class::COLLECTION_FILTERS.each do |filter_name, _| %>
<option value="<%= filter_name %>" <%= 'selected' if filter_name.to_s == current_filter %>>
<%= filter_name.to_s.titleize %>
</option>
<% end %>
</select>
<% if current_filter %>
<a href="?" class="absolute right-6 text-n-slate-10 hover:text-n-slate-12 text-lg font-medium" title="Clear filter">&times;</a>
<% end %>
</div>
<script>
@@ -1,7 +1,9 @@
<li class="px-4 mb-1">
<% text_class_name = current_page?(url) ? 'text-woot-500 bg-slate-25' : 'text-slate-800' %>
<%= link_to(url, class: text_class_name + " -ml-1 focus:outline-none cursor-pointer flex items-center px-2 py-1.5 text-slate-800 cursor-pointer hover:text-woot-500 hover:bg-slate-25 rounded-lg") do %>
<svg width="16" height="16"><use xlink:href="#<%= icon %>" /></svg>
<span class="ml-2 text-sm"><%= label %></span>
<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" %>
<%= link_to(url, class: "#{base_class} #{active_class}") do %>
<svg width="20" height="20" class="flex-shrink-0"><use xlink:href="#<%= icon %>" /></svg>
<span><%= label %></span>
<% end %>
</li>
@@ -19,18 +19,18 @@ as defined by the routes in the `admin/` namespace
}
%>
<div class="border-slate-100 border-r w-56 flex-shrink-0 justify-between h-full flex flex-col" role="navigation">
<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>
<div class="flex mx-4 mb-4 border-slate-100 border-b py-6">
<%= link_to image_tag('/brand-assets/logo_thumbnail.svg', alt: 'Chatwoot Admin Dashboard', class: 'h-10'), super_admin_root_url %>
<div class="flex flex-col ml-3">
<div class="text-sm">Chatwoot <%= Chatwoot.config[:version] %></div>
<div class="text-xs text-slate-700 mt-0.5">Super Admin Console</div>
<div class="flex items-center mx-4 border-n-weak border-b py-5">
<%= 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>
<div class="text-xs text-n-slate-11">Super Admin Console</div>
</div>
</div>
<ul class="my-4">
<%= render partial: "nav_item", locals: { icon: 'icon-grid-line', url: super_admin_root_url, label: 'Dashboard' } %>
<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' } %>
<% 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: {
@@ -44,11 +44,11 @@ as defined by the routes in the `admin/` namespace
<%= render 'settings_menu', open: settings_open? %>
</ul>
</div>
<div>
<ul class="my-4">
<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-dashboard-line', url: '/', label: 'Agent Dashboard' } %>
<%= 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' } %>
</ul>
</div>
@@ -1,6 +1,6 @@
<form class="search" role="search">
<label class="search__label" for="search">
<svg class="search__eyeglass-icon" role="img">
<form class="relative flex items-center" role="search">
<label class="absolute left-3 text-n-slate-10" for="search">
<svg class="w-4 h-4" role="img">
<title>
<%= t("administrate.search.label", resource: resource_name) %>
</title>
@@ -8,17 +8,19 @@
</svg>
</label>
<input class="search__input"
<input class="h-9 w-[200px] pl-9 pr-8 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 placeholder:text-n-slate-10"
id="search"
type="search"
name="search"
placeholder="<%= t("administrate.search.label", resource: resource_name) %>"
value="<%= search_term %>">
<%= link_to clear_search_params, class: "search__clear-link" do %>
<svg class="search__clear-icon" role="img">
<title><%= t("administrate.search.clear") %></title>
<use xlink:href="#icon-cancel" />
</svg>
<% if search_term.present? %>
<%= link_to clear_search_params, class: "absolute right-2 text-n-slate-10 hover:text-n-slate-12" do %>
<svg class="w-4 h-4" role="img">
<title><%= t("administrate.search.clear") %></title>
<use xlink:href="#icon-cancel" />
</svg>
<% end %>
<% end %>
</form>
@@ -1,21 +1,23 @@
<li class="px-4 mb-1">
<li>
<details class="group" <%= 'open' if open %>>
<summary class="-ml-1 flex items-center px-2 py-1.5 cursor-pointer rounded-lg <%= open ? 'text-woot-500 bg-slate-25' : 'text-slate-800 hover:text-woot-500 hover:bg-slate-25' %> list-none">
<%= link_to super_admin_settings_url, class: 'flex items-center flex-1' do %>
<svg width="16" height="16"><use xlink:href="#icon-settings-2-line" /></svg>
<span class="ml-2 text-sm">Settings</span>
<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' %>">
<%= 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>
<span>Settings</span>
<% end %>
<svg class="ml-auto w-4 h-4 transition-transform duration-200 group-open:rotate-180" viewBox="0 0 20 20" fill="currentColor">
<svg class="w-4 h-4 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="ml-4 mt-1">
<ul class="mt-1 ml-7 space-y-0.5">
<% settings_pages.each do |_feature_key, attrs| %>
<% url = super_admin_app_config_url(config: attrs['config_key']) %>
<li class="px-4 mb-1">
<% text_class = current_page?(url) ? 'text-woot-500 bg-slate-25' : 'text-slate-800' %>
<%= link_to url, class: text_class + ' -ml-1 flex items-center px-2 py-1.5 hover:text-woot-500 hover:bg-slate-25 rounded-lg' do %>
<span class="ml-2 text-sm"><%= attrs['name'] %></span>
<li>
<% 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>
<% end %>
</li>
<% end %>
@@ -27,49 +27,70 @@ It renders the `_table` partial to display details about the resources.
<%= display_resource_name(page.resource_name) %>
<% end %>
<header class="main-content__header" role="banner">
<div class="flex items-center justify-between w-full">
<h1 class="main-content__page-title m-0 mr-6" id="page-title">
<%= content_for(:title) %>
</h1>
<%
resource_icons = {
'account' => 'icon-building-4-line',
'user' => 'icon-user-follow-line',
'account_user' => 'icon-user-follow-line',
'agent_bot' => 'icon-robot-line',
'response_source' => 'icon-book-2-line',
'response_document' => 'icon-draft-line',
'response' => 'icon-reply-line'
}
current_icon = resource_icons[page.resource_name] || 'icon-apps-2-line'
%>
<div class="flex items-center">
<% if show_search_bar %>
<div class="flex items-center">
<%= render("filters", page: page) %>
<div class="ml-3">
<%= render(
"search",
search_term: search_term,
resource_name: display_resource_name(page.resource_name)
) %>
</div>
<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 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>
</div>
<% end %>
<div class="flex flex-col justify-center">
<h1 class="text-base font-medium tracking-tight text-n-slate-12" id="page-title">
<%= content_for(:title) %>
</h1>
<p class="text-xs text-n-slate-11 m-0">Manage all registered <%= page.resource_name.pluralize.downcase %></p>
</div>
</div>
<div class="whitespace-nowrap ml-4">
<%= link_to(
t(
"administrate.actions.new_resource",
name: page.resource_name.titleize.downcase
),
[:new, namespace, page.resource_path.to_sym],
class: "button",
) if accessible_action?(new_resource, :new) %>
<div class="flex items-center gap-3">
<% if show_search_bar %>
<%= render("filters", page: page) %>
<%= render(
"search",
search_term: search_term,
resource_name: display_resource_name(page.resource_name)
) %>
<% end %>
<% if accessible_action?(new_resource, :new) %>
<%= link_to(
t(
"administrate.actions.new_resource",
name: page.resource_name.titleize.downcase
),
[:new, namespace, page.resource_path.to_sym],
class: "inline-flex items-center justify-center h-8 px-3 text-sm font-medium text-white bg-woot-500 rounded-lg hover:bg-woot-600 transition-colors",
) %>
<% end %>
</div>
</div>
</div>
</header>
</header>
<section class="main-content__body main-content__body--flush">
<%= render(
"collection",
collection_presenter: page,
collection_field_name: resource_name,
page: page,
resources: resources,
table_title: "page-title"
) %>
<main class="flex-1 px-6 overflow-y-auto">
<div class="w-full mx-auto max-w-[60rem] py-4">
<%= render(
"collection",
collection_presenter: page,
collection_field_name: resource_name,
page: page,
resources: resources,
table_title: "page-title"
) %>
<%= paginate resources, param_name: '_page'%>
<%= paginate resources, param_name: '_page'%>
</div>
</main>
</section>
+78 -40
View File
@@ -18,48 +18,86 @@ as well as a link to its edit page.
<% content_for(:title) { t("administrate.actions.show_resource", name: page.page_title) } %>
<header class="main-content__header">
<h1 class="main-content__page-title">
<%= content_for(:title) %>
</h1>
<%
resource_icons = {
'account' => 'icon-building-4-line',
'user' => 'icon-user-follow-line',
'account_user' => 'icon-user-follow-line',
'agent_bot' => 'icon-robot-line',
'response_source' => 'icon-book-2-line',
'response_document' => 'icon-draft-line',
'response' => 'icon-reply-line'
}
current_icon = resource_icons[page.resource_name] || 'icon-folder-3-line'
%>
<div>
<%= link_to(
"Edit",
[:edit, namespace, page.resource],
class: "button",
) if accessible_action?(page.resource, :edit) %>
<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 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>
</div>
<div class="flex flex-col justify-center">
<h1 class="text-base font-medium tracking-tight text-n-slate-12">
<%= content_for(:title) %>
</h1>
<p class="text-xs text-n-slate-11 m-0">Resource details and configuration</p>
</div>
</div>
<%= link_to(
t("administrate.actions.destroy"),
[namespace, page.resource],
class: "button button--danger",
method: :delete,
data: { confirm: t("administrate.actions.confirm") }
) if accessible_action?(page.resource, :destroy) %>
</div>
</header>
<section class="main-content__body">
<dl>
<% page.attributes.each do |title, attributes| %>
<fieldset class="<%= "field-unit--nested" if title.present? %>">
<% if title.present? %>
<legend><%= t "helpers.label.#{page.resource_name}.#{title}", default: title %></legend>
<% end %>
<% attributes.each do |attribute| %>
<dt class="attribute-label" id="<%= attribute.name %>">
<%= t(
"helpers.label.#{resource_name}.#{attribute.name}",
default: page.resource.class.human_attribute_name(attribute.name),
<div class="flex items-center gap-2">
<% if accessible_action?(page.resource, :edit) %>
<%= link_to(
t("administrate.actions.edit"),
[:edit, namespace, page.resource],
class: "inline-flex items-center justify-center h-8 px-3 text-sm font-medium text-white bg-woot-500 rounded-lg hover:bg-woot-600 transition-colors"
) %>
</dt>
<dd class="attribute-data attribute-data--<%=attribute.html_class%>"
><%= render_field attribute, page: page %></dd>
<% end %>
</fieldset>
<% end %>
</dl>
<% if accessible_action?(page.resource, :destroy) %>
<%= link_to(
t("administrate.actions.destroy"),
[namespace, page.resource],
class: "inline-flex items-center justify-center h-8 px-3 text-sm font-medium text-ruby-500 bg-ruby-100 rounded-lg hover:bg-ruby-200 transition-colors",
method: :delete,
data: { confirm: t("administrate.actions.confirm") }
) %>
<% end %>
</div>
</div>
</header>
<main class="flex-1 px-6 overflow-y-auto">
<div class="w-full mx-auto max-w-[60rem] py-6 space-y-6">
<% page.attributes.each do |title, attributes| %>
<div class="rounded-xl bg-n-solid-2 shadow outline-1 outline outline-n-container overflow-hidden">
<div class="px-6 py-4 border-b border-n-weak">
<% if title.present? %>
<h2 class="text-base font-medium text-n-slate-12">
<%= t "helpers.label.#{page.resource_name}.#{title}", default: title.to_s.titleize %>
</h2>
<% else %>
<h2 class="text-base font-medium text-n-slate-12">Details</h2>
<% end %>
</div>
<div class="divide-y divide-n-weak">
<% attributes.each do |attribute| %>
<div class="grid grid-cols-[180px,1fr] items-center min-h-12 px-6">
<span class="text-sm text-n-slate-11">
<%= t(
"helpers.label.#{resource_name}.#{attribute.name}",
default: page.resource.class.human_attribute_name(attribute.name),
) %>
</span>
<span class="text-sm text-n-slate-12 py-3">
<%= render_field attribute, page: page %>
</span>
</div>
<% end %>
</div>
</div>
<% end %>
</div>
</main>
</section>
@@ -1,23 +1,49 @@
<% content_for(:title) do %>
Instance Status
<% end %>
<header class="main-content__header" role="banner">
<h1 class="main-content__page-title" id="page-title">
<%= content_for(:title) %>
</h1>
</header>
<section class="main-content__body">
<table>
<tr>
<th>Metric</th>
<th>Value</th>
</tr>
<% @metrics.each do |key,value| %>
<tr>
<td> <%= key %> </td>
<td> <%= value %></td>
</tr>
<% end %>
</table>
<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 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>
</div>
<div class="flex flex-col justify-center">
<h1 class="text-base font-medium tracking-tight text-n-slate-12" id="page-title">
<%= content_for(:title) %>
</h1>
<p class="text-xs text-n-slate-11 m-0">Chatwoot version and system information</p>
</div>
</div>
</div>
</header>
<main class="flex-1 px-6 overflow-y-auto">
<div class="w-full mx-auto max-w-[60rem] py-6">
<div class="rounded-xl bg-n-solid-2 shadow outline-1 outline outline-n-container overflow-hidden">
<div class="px-6 py-4 border-b border-n-weak">
<h2 class="text-base font-medium text-n-slate-12">System Metrics</h2>
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-n-slate-1">
<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>
</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>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
</main>
</section>
+120 -113
View File
@@ -1,129 +1,136 @@
<% content_for(:title) do %>
Settings
<% end %>
<header class="flex px-8 py-4 items-center border-b border-n-weak" role="banner">
<div class="border border-n-weak mr-4 p-2 rounded-full">
<svg width="24" height="24"><use xlink:href="#icon-settings-2-line" /></svg>
</div>
<div class="flex flex-col h-14 justify-center">
<h1 class="text-base font-medium leading-6 text-n-slate-12" id="page-title">
<%= content_for(:title) %>
</h1>
<p class="text-sm font-normal leading-5 text-slate-500 m-0">Update your instance settings, access billing portal</p>
</div>
</header>
<section class="main-content__body px-8">
<% if Redis::Alfred.get(Redis::Alfred::CHATWOOT_INSTALLATION_CONFIG_RESET_WARNING) %>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-5" role="alert">
<strong class="font-bold">Alert!</strong>
<span class="block sm:inline">Unauthorized premium changes detected in Chatwoot. To keep using them, please upgrade your plan.
Contact for help :</span><span class="inline rounded-full bg-red-200 px-2 text-white ml-2">sales@chatwoot.com</span>
</div>
<% end %>
<div class="bg-white py-2 px-4 xl:px-0">
<%= javascript_include_tag "secretField" %>
<div class="flex items-center mb-6">
<h4 class="text-sm font-medium leading-5 h-5 text-n-slate-12 mr-4">Installation Identifier</h4>
<div data-secret-text="<%= ChatwootHub.installation_identifier %>" class="cell-data__secret-field text-sm leading-5 h-5 text-n-slate-11">
<span data-secret-masked="true" class="select-none"></span>
<button type="button" onclick="toggleSecretField(event)" data-secret-toggler class="inline-flex items-center justify-center h-5 w-5 p-0 rounded text-n-slate-11 hover:text-n-slate-12 transition-colors focus:outline-none">
<svg width="20" height="20" class="w-5 h-5 fill-current">
<use xlink:href="#eye-show" />
</svg>
</button>
<button type="button" onclick="copySecretField(event)" data-secret-copier class="inline-flex items-center justify-center h-5 w-5 p-0 rounded text-n-slate-11 hover:text-n-slate-12 transition-colors focus:outline-none">
<svg width="20" height="20" class="w-5 h-5 fill-current">
<use xlink:href="#icon-copy" />
</svg>
</button>
<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="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>
</div>
<div class="flex flex-col justify-center">
<h1 class="text-base font-medium tracking-tight text-n-slate-12" id="page-title">
<%= content_for(:title) %>
</h1>
<p class="text-xs text-n-slate-11 m-0">Update your instance settings, access billing portal</p>
</div>
</div>
<% if ChatwootApp.enterprise? %>
<div class="flex p-4 outline outline-1 outline-n-container rounded-lg items-start md:items-center shadow-sm flex-col md:flex-row">
<div class="flex flex-col flex-grow gap-1">
<div class="flex items-center gap-2">
<h2 class="h-5 leading-5 text-n-slate-12 text-sm font-medium">Current plan</h2>
<a href="<%= refresh_super_admin_settings_url %>" class="inline-flex gap-1 text-xs font-medium items-center text-woot-500 hover:text-woot-700">
<svg width="16" height="16"><use xlink:href="#icon-refresh-line" /></svg>
<span>Refresh</span>
</a>
</div>
<p class="text-n-slate-11 m-0 text-sm"><%= SuperAdmin::FeaturesHelper.plan_details.html_safe %></p>
</header>
<main class="flex-1 px-6 overflow-y-auto">
<div class="w-full mx-auto max-w-[60rem] py-4">
<% if Redis::Alfred.get(Redis::Alfred::CHATWOOT_INSTALLATION_CONFIG_RESET_WARNING) %>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-5" role="alert">
<strong class="font-bold">Alert!</strong>
<span class="block sm:inline">Unauthorized premium changes detected in Chatwoot. To keep using them, please upgrade your plan.
Contact for help :</span><span class="inline rounded-full bg-red-200 px-2 text-white ml-2">sales@chatwoot.com</span>
</div>
<a href="<%= ChatwootHub.billing_url %>" target="_blank" rel="noopener noreferrer">
<button class="mt-4 md:mt-0 flex gap-1 items-center bg-transparent shadow-sm h-9 hover:text-n-slate-12 hover:bg-slate-50 outline outline-1 outline-n-container rounded text-n-slate-11 font-medium p-2 focus:outline-none">
<svg width="16" height="16"><use xlink:href="#icon-settings-2-line" /></svg>
<span class="px-1">Manage</span>
<% end %>
<%= javascript_include_tag "secretField" %>
<div class="flex items-center mb-6">
<h4 class="text-sm font-medium leading-5 h-5 text-n-slate-12 mr-4">Installation Identifier</h4>
<div data-secret-text="<%= ChatwootHub.installation_identifier %>" class="cell-data__secret-field text-sm leading-5 h-5 text-n-slate-11">
<span data-secret-masked="true" class="select-none"></span>
<button type="button" onclick="toggleSecretField(event)" data-secret-toggler class="inline-flex items-center justify-center h-5 w-5 p-0 rounded text-n-slate-11 hover:text-n-slate-12 transition-colors focus:outline-none">
<svg width="20" height="20" class="w-5 h-5 fill-current">
<use xlink:href="#eye-show" />
</svg>
</button>
<button type="button" onclick="copySecretField(event)" data-secret-copier class="inline-flex items-center justify-center h-5 w-5 p-0 rounded text-n-slate-11 hover:text-n-slate-12 transition-colors focus:outline-none">
<svg width="20" height="20" class="w-5 h-5 fill-current">
<use xlink:href="#icon-copy" />
</svg>
</button>
</div>
</div>
<% if ChatwootApp.enterprise? %>
<div class="flex p-4 outline outline-1 outline-n-container rounded-xl items-start md:items-center shadow-sm flex-col md:flex-row bg-n-solid-2">
<div class="flex flex-col flex-grow gap-1">
<div class="flex items-center gap-2">
<h2 class="h-5 leading-5 text-n-slate-12 text-sm font-medium">Current plan</h2>
<a href="<%= refresh_super_admin_settings_url %>" class="inline-flex gap-1 text-xs font-medium items-center text-woot-500 hover:text-woot-700">
<svg width="16" height="16"><use xlink:href="#icon-refresh-line" /></svg>
<span>Refresh</span>
</a>
</div>
<p class="text-n-slate-11 m-0 text-sm"><%= SuperAdmin::FeaturesHelper.plan_details.html_safe %></p>
</div>
<a href="<%= ChatwootHub.billing_url %>" target="_blank" rel="noopener noreferrer">
<button class="mt-4 md:mt-0 flex gap-1 items-center bg-transparent shadow-sm h-9 hover:text-n-slate-12 hover:bg-n-alpha-2 outline outline-1 outline-n-container rounded text-n-slate-11 font-medium p-2 focus:outline-none">
<svg width="16" height="16"><use xlink:href="#icon-settings-2-line" /></svg>
<span class="px-1">Manage</span>
</button>
</a>
</div>
<% end %>
<% if ChatwootHub.pricing_plan != 'community' && User.count > ChatwootHub.pricing_plan_quantity %>
<div role="alert" class="mt-4">
<div class="border border-t-0 border-red-400 rounded-b bg-red-100 px-4 py-3 text-red-700">
<p>You have <%= User.count %> agents. Please add more licenses to add more users.</p>
</div>
</div>
<% end %>
<div class="flex p-4 outline outline-1 outline-n-container rounded-xl mt-4 items-start md:items-center shadow-sm flex-col md:flex-row bg-n-solid-2">
<div class="flex flex-col flex-grow gap-1">
<h2 class="h-5 leading-5 text-n-slate-12 text-sm font-medium">Need help?</h2>
<p class="text-n-slate-11 m-0 text-sm">Do you face any issues? We are here to help.</p>
</div>
<a href="https://discord.gg/cJXdrwS" target="_blank">
<button class="flex mt-4 md:mt-0 gap-1 items-center bg-transparent shadow-sm h-9 bg-violet-500 hover:bg-violet-600 text-white border border-solid border-violet-600 rounded font-medium p-2 focus:outline-none">
<svg class="h-4 w-4" width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M13.209 3.103c-.495-1.004-1.926-1.004-2.421 0L8.43 7.88l-5.273.766c-1.107.161-1.55 1.522-.748 2.303l3.815 3.72-.9 5.25c-.19 1.103.968 1.944 1.959 1.424l4.715-2.48 4.716 2.48c.99.52 2.148-.32 1.96-1.424l-.902-5.25 3.816-3.72c.8-.78.359-2.142-.748-2.303l-5.273-.766-2.358-4.777ZM9.74 8.615l2.258-4.576 2.259 4.576a1.35 1.35 0 0 0 1.016.738l5.05.734-3.654 3.562a1.35 1.35 0 0 0-.388 1.195l.862 5.03-4.516-2.375a1.35 1.35 0 0 0-1.257 0l-4.516 2.374.862-5.029a1.35 1.35 0 0 0-.388-1.195l-3.654-3.562 5.05-.734c.44-.063.82-.34 1.016-.738ZM1.164 3.782a.75.75 0 0 0 .118 1.054l2.5 2a.75.75 0 1 0 .937-1.172l-2.5-2a.75.75 0 0 0-1.055.118Z" fill="currentColor"/><path d="M22.836 18.218a.75.75 0 0 0-.117-1.054l-2.5-2a.75.75 0 0 0-.938 1.172l2.5 2a.75.75 0 0 0 1.055-.117ZM1.282 17.164a.75.75 0 1 0 .937 1.172l2.5-2a.75.75 0 0 0-.937-1.172l-2.5 2ZM22.836 3.782a.75.75 0 0 1-.117 1.054l-2.5 2a.75.75 0 0 1-.938-1.172l2.5-2a.75.75 0 0 1 1.055.118Z" fill="currentColor"/></svg>
<span class="px-1">Community Support</span>
</button>
</a>
<% if ChatwootHub.pricing_plan != 'community' %>
<button class="ml-4 flex gap-1 items-center bg-transparent h-9 hover:text-n-slate-12 hover:bg-n-alpha-2 border border-solid border-n-weak rounded text-n-slate-11 font-medium p-2 focus:outline-none" onclick="window.$chatwoot.toggle('open')">
<svg class="h-4 w-4" width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M13.209 3.103c-.495-1.004-1.926-1.004-2.421 0L8.43 7.88l-5.273.766c-1.107.161-1.55 1.522-.748 2.303l3.815 3.72-.9 5.25c-.19 1.103.968 1.944 1.959 1.424l4.715-2.48 4.716 2.48c.99.52 2.148-.32 1.96-1.424l-.902-5.25 3.816-3.72c.8-.78.359-2.142-.748-2.303l-5.273-.766-2.358-4.777ZM9.74 8.615l2.258-4.576 2.259 4.576a1.35 1.35 0 0 0 1.016.738l5.05.734-3.654 3.562a1.35 1.35 0 0 0-.388 1.195l.862 5.03-4.516-2.375a1.35 1.35 0 0 0-1.257 0l-4.516 2.374.862-5.029a1.35 1.35 0 0 0-.388-1.195l-3.654-3.562 5.05-.734c.44-.063.82-.34 1.016-.738ZM1.164 3.782a.75.75 0 0 0 .118 1.054l2.5 2a.75.75 0 1 0 .937-1.172l-2.5-2a.75.75 0 0 0-1.055.118Z" fill="currentColor"/><path d="M22.836 18.218a.75.75 0 0 0-.117-1.054l-2.5-2a.75.75 0 0 0-.938 1.172l2.5 2a.75.75 0 0 0 1.055-.117ZM1.282 17.164a.75.75 0 1 0 .937 1.172l2.5-2a.75.75 0 0 0-.937-1.172l-2.5 2ZM22.836 3.782a.75.75 0 0 1-.117 1.054l-2.5 2a.75.75 0 0 1-.938-1.172l2.5-2a.75.75 0 0 1 1.055.118Z" fill="currentColor"/></svg>
<span class="px-1">Chat Support</span>
</button>
<% end %>
</div>
<% end %>
<% if ChatwootHub.pricing_plan != 'community' && User.count > ChatwootHub.pricing_plan_quantity %>
<div role="alert">
<div class="border border-t-0 border-red-400 rounded-b bg-red-100 px-4 py-3 text-red-700">
<p>You have <%= User.count %> agents. Please add more licenses to add more users.</p>
<div class="mt-10 py-4">
<h3 class="h-5 leading-5 text-n-slate-12 font-medium text-base">Features</h3>
</div>
</div>
<% end %>
<div class="flex p-4 outline outline-1 outline-n-container rounded-lg mt-4 items-start md:items-center shadow-sm flex-col md:flex-row">
<div class="flex flex-col flex-grow gap-1">
<h2 class="h-5 leading-5 text-n-slate-12 text-sm font-medium">Need help?</h2>
<p class="text-n-slate-11 m-0 text-sm">Do you face any issues? We are here to help.</p>
</div>
<a href="https://discord.gg/cJXdrwS" target="_blank">
<button class="flex mt-4 md:mt-0 gap-1 items-center bg-transparent shadow-sm h-9 bg-violet-500 hover:bg-violet-600 text-white border border-solid border-violet-600 rounded font-medium p-2 focus:outline-none">
<svg class="h-4 w-4" width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M13.209 3.103c-.495-1.004-1.926-1.004-2.421 0L8.43 7.88l-5.273.766c-1.107.161-1.55 1.522-.748 2.303l3.815 3.72-.9 5.25c-.19 1.103.968 1.944 1.959 1.424l4.715-2.48 4.716 2.48c.99.52 2.148-.32 1.96-1.424l-.902-5.25 3.816-3.72c.8-.78.359-2.142-.748-2.303l-5.273-.766-2.358-4.777ZM9.74 8.615l2.258-4.576 2.259 4.576a1.35 1.35 0 0 0 1.016.738l5.05.734-3.654 3.562a1.35 1.35 0 0 0-.388 1.195l.862 5.03-4.516-2.375a1.35 1.35 0 0 0-1.257 0l-4.516 2.374.862-5.029a1.35 1.35 0 0 0-.388-1.195l-3.654-3.562 5.05-.734c.44-.063.82-.34 1.016-.738ZM1.164 3.782a.75.75 0 0 0 .118 1.054l2.5 2a.75.75 0 1 0 .937-1.172l-2.5-2a.75.75 0 0 0-1.055.118Z" fill="currentColor"/><path d="M22.836 18.218a.75.75 0 0 0-.117-1.054l-2.5-2a.75.75 0 0 0-.938 1.172l2.5 2a.75.75 0 0 0 1.055-.117ZM1.282 17.164a.75.75 0 1 0 .937 1.172l2.5-2a.75.75 0 0 0-.937-1.172l-2.5 2ZM22.836 3.782a.75.75 0 0 1-.117 1.054l-2.5 2a.75.75 0 0 1-.938-1.172l2.5-2a.75.75 0 0 1 1.055.118Z" fill="currentColor"/></svg>
<span class="px-1">Community Support</span>
</button>
</a>
<% if ChatwootHub.pricing_plan !='community' %>
<button class="ml-4 flex gap-1 items-center bg-transparent h-9 hover:text-n-slate-12 hover:bg-slate-50 border border-solid border-slate-100 rounded text-n-slate-11 font-medium p-2 focus:outline-none" onclick="window.$chatwoot.toggle('open')">
<svg class="h-4 w-4" width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M13.209 3.103c-.495-1.004-1.926-1.004-2.421 0L8.43 7.88l-5.273.766c-1.107.161-1.55 1.522-.748 2.303l3.815 3.72-.9 5.25c-.19 1.103.968 1.944 1.959 1.424l4.715-2.48 4.716 2.48c.99.52 2.148-.32 1.96-1.424l-.902-5.25 3.816-3.72c.8-.78.359-2.142-.748-2.303l-5.273-.766-2.358-4.777ZM9.74 8.615l2.258-4.576 2.259 4.576a1.35 1.35 0 0 0 1.016.738l5.05.734-3.654 3.562a1.35 1.35 0 0 0-.388 1.195l.862 5.03-4.516-2.375a1.35 1.35 0 0 0-1.257 0l-4.516 2.374.862-5.029a1.35 1.35 0 0 0-.388-1.195l-3.654-3.562 5.05-.734c.44-.063.82-.34 1.016-.738ZM1.164 3.782a.75.75 0 0 0 .118 1.054l2.5 2a.75.75 0 1 0 .937-1.172l-2.5-2a.75.75 0 0 0-1.055.118Z" fill="currentColor"/><path d="M22.836 18.218a.75.75 0 0 0-.117-1.054l-2.5-2a.75.75 0 0 0-.938 1.172l2.5 2a.75.75 0 0 0 1.055-.117ZM1.282 17.164a.75.75 0 1 0 .937 1.172l2.5-2a.75.75 0 0 0-.937-1.172l-2.5 2ZM22.836 3.782a.75.75 0 0 1-.117 1.054l-2.5 2a.75.75 0 0 1-.938-1.172l2.5-2a.75.75 0 0 1 1.055.118Z" fill="currentColor"/></svg>
<span class="px-1">Chat Support</span>
</button>
<% end %>
</div>
<div class="mt-10 py-4">
<h3 class="h-5 leading-5 text-n-slate-12 font-medium text-base">Features</h3>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-2 gap-y-3">
<% SuperAdmin::FeaturesHelper.available_features.each do |feature, attrs| %>
<div class="group outline outline-1 outline-n-container p-4 rounded-lg relative shadow-sm">
<span class="<%= attrs[:enabled] ? 'bg-green-400 text-white': 'bg-slate-300 text-slate-800' %> absolute rounded-full p-1 inline-flex right-4 top-5">
<svg width="14" height="14"><use xlink:href="<%= attrs[:enabled] ? '#icon-tick-line': '#icon-lock-line' %>" /></svg>
</span>
<div class="visible <% if !attrs[:enabled] %> group-hover:invisible <% end %> flex items-center justify-center w-10 h-10 mb-2 border border-slate-100 border-solid text-slate-800 rounded-full">
<svg width="20" height="20"><use xlink:href="#<%= attrs[:icon] %>" /></svg>
</div>
<% if !attrs[:enabled] %>
<div class="flex h-9 absolute top-5 items-center invisible group-hover:visible">
<% if ChatwootApp.enterprise? %>
<%= render 'upgrade_button_enterprise' %>
<% else %>
<%= render 'upgrade_button_community' %>
<% end %>
</div>
<% end %>
<div class="flex items-center justify-between mb-1.5 mt-4">
<div class="flex items-center gap-2">
<h3 class="text-n-slate-12 font-medium"><%= attrs[:name] %></h3>
<% if attrs[:enterprise] %>
<span class="px-2 h-4 leading-4 rounded-xl text-green-800 font-medium bg-green-100/70 text-xxs">EE</span>
<% end %>
<% if attrs[:config_key].present? && attrs[:enabled] %>
<a class="inline-flex items-center justify-center h-5 w-5 hover:bg-slate-50 rounded-sm" href="/super_admin/app_config?config=<%= attrs[:config_key] %>">
<svg width="18" height="18"><use xlink:href="#icon-gear" /></svg>
</a>
<% end %>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<% SuperAdmin::FeaturesHelper.available_features.each do |feature, attrs| %>
<div class="group outline outline-1 outline-n-container p-4 rounded-xl relative shadow-sm bg-n-solid-2">
<span class="<%= attrs[:enabled] ? 'bg-green-400 text-white': 'bg-slate-300 text-slate-800' %> absolute rounded-full p-1 inline-flex right-4 top-5">
<svg width="14" height="14"><use xlink:href="<%= attrs[:enabled] ? '#icon-tick-line': '#icon-lock-line' %>" /></svg>
</span>
<div class="visible <% if !attrs[:enabled] %> group-hover:invisible <% end %> flex items-center justify-center w-10 h-10 mb-2 border border-n-weak border-solid text-n-slate-11 rounded-full">
<svg width="20" height="20"><use xlink:href="#<%= attrs[:icon] %>" /></svg>
</div>
<% if !attrs[:enabled] %>
<div class="flex h-9 absolute top-5 items-center invisible group-hover:visible">
<% if ChatwootApp.enterprise? %>
<%= render 'upgrade_button_enterprise' %>
<% else %>
<%= render 'upgrade_button_community' %>
<% end %>
</div>
<% end %>
<div class="flex items-center justify-between mb-1.5 mt-4">
<div class="flex items-center gap-2">
<h3 class="text-n-slate-12 font-medium"><%= attrs[:name] %></h3>
<% if attrs[:enterprise] %>
<span class="px-2 h-4 leading-4 rounded-xl text-green-800 font-medium bg-green-100/70 text-xxs">EE</span>
<% end %>
<% if attrs[:config_key].present? && attrs[:enabled] %>
<a class="inline-flex items-center justify-center h-5 w-5 hover:bg-n-alpha-2 rounded-sm" href="/super_admin/app_config?config=<%= attrs[:config_key] %>">
<svg width="18" height="18"><use xlink:href="#icon-gear" /></svg>
</a>
<% end %>
</div>
</div>
<p class="text-n-slate-11 mb-0 text-sm"><%= attrs[:description] %></p>
</div>
<p class="text-n-slate-11 mb-0"><%= attrs[:description] %></p>
</div>
<% end %>
<% end %>
</div>
</div>
</div>
</main>
</section>
@@ -11,44 +11,18 @@ Used by both account and user show pages.
- `resource_type`: Either 'account' or 'user' to determine hidden field
%>
<section class="main-content__body">
<% account_user_page = Administrate::Page::Form.new(AccountUserDashboard.new, AccountUser.new) %>
<%= form_for([namespace, account_user_page.resource], html: { class: "form" }) do |f| %>
<% if account_user_page.resource.errors.any? %>
<div id="error_explanation">
<h2>
<%= t(
"administrate.form.errors",
pluralized_errors: pluralize(
account_user_page.resource.errors.count,
t("administrate.form.error")
),
resource_name: display_resource_name(account_user_page.resource_name)
) %>
</h2>
<ul>
<% account_user_page.resource.errors.full_messages.each do |message| %>
<li class="flash-error"><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<% account_user_page = Administrate::Page::Form.new(AccountUserDashboard.new, AccountUser.new) %>
<%= form_for([namespace, account_user_page.resource]) do |f| %>
<div class="flex items-end gap-4 flex-wrap">
<% account_user_page.attributes.each do |title, attributes| -%>
<% attributes.each do |attribute| %>
<% if attribute.name == resource_type %>
<% if attribute.name.to_s == resource_type %>
<%= f.hidden_field("#{resource_type}_id", value: page.resource.id) %>
<% else %>
<div class="field-unit field-unit--<%= attribute.html_class %> field-unit--<%= requireness(attribute) %>">
<%= render_field attribute, f: f %>
</div>
<%= render_field attribute, f: f %>
<% end %>
<% end %>
<% end -%>
<div class="form-actions">
<%= f.submit %>
</div>
<% end %>
</section>
<%= f.submit "Add User", class: "inline-flex items-center justify-center h-9 px-4 text-sm font-medium text-white bg-woot-500 rounded-lg hover:bg-woot-600 transition-colors cursor-pointer" %>
</div>
<% end %>
@@ -18,78 +18,80 @@ to display a collection of resources in an HTML table.
[1]: http://www.rubydoc.info/gems/administrate/Administrate/Page/Collection
%>
<table aria-labelledby="<%= table_title %>">
<thead>
<tr>
<% collection_presenter.attribute_types.each do |attr_name, attr_type| %>
<th class="cell-label
cell-label--<%= attr_type.html_class %>
cell-label--<%= collection_presenter.ordered_html_class(attr_name) %>"
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)
)) 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="cell-label__sort-indicator cell-label__sort-indicator--<%= collection_presenter.ordered_html_class(attr_name) %>">
<svg aria-hidden="true">
<use xlink:href="#icon-up-caret" />
</svg>
</span>
<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 %>
<% end %>
</th>
<% 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>
<% 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 %>
<% end %>
</th>
<% end %>
<% [existing_action?(collection_presenter.resource_name, :edit),
existing_action?(collection_presenter.resource_name, :destroy)].count(true).times do %>
<th scope="col"></th>
<% end %>
</tr>
</thead>
<tbody>
<% resources.each do |resource| %>
<tr class="js-table-row"
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="cell-data cell-data--<%= attribute.html_class %>">
<% if authorized_action? resource, :show -%>
<a href="<%= polymorphic_path([namespace, resource.becomes(User)]) -%>"
class="action-show"
>
<%= 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, :edit %>
<td><%= link_to(
t("administrate.actions.edit"),
[:edit, namespace, resource.becomes(User)],
class: "action-edit",
) if authorized_action? resource, :edit%></td>
<% 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 %>
<% if existing_action? collection_presenter.resource_name, :destroy %>
<td><%= link_to(
t("administrate.actions.destroy"),
[namespace, collection_presenter.resource_name == 'account_user' ? resource : resource.becomes(User)],
class: "text-color-red",
method: :delete,
data: { confirm: t("administrate.actions.confirm") }
) if authorized_action? resource, :destroy %></td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
</tbody>
</table>
</div>
</div>
+7 -7
View File
@@ -1,8 +1,8 @@
<section class="main-content__body">
<hr/>
<div class="form-actions">
<p class="text-color-red">Caution: Any actions executed after impersonate will appear as if performed by the impersonated user - [<%= page.resource.name %> ]</p>
<a class='button' target='_blank' href='<%= page.resource.generate_sso_link_with_impersonation %>'>Impersonate user </a>
</div>
</section>
<div class="px-6 py-4">
<h3 class="text-sm font-medium text-n-slate-12 mb-2">Impersonate User</h3>
<p class="text-xs text-ruby-500 mb-3">Caution: Any actions executed after impersonate will appear as if performed by the impersonated user - [<%= page.resource.name %>]</p>
<a target="_blank" href="<%= page.resource.generate_sso_link_with_impersonation %>" class="inline-flex items-center justify-center h-8 px-3 text-sm font-medium text-n-slate-12 bg-n-alpha-2 rounded-lg hover:bg-n-alpha-3 transition-colors">
Impersonate user
</a>
</div>
+143 -36
View File
@@ -18,44 +18,151 @@ as well as a link to its edit page.
<% content_for(:title) { t("administrate.actions.show_resource", name: page.page_title) } %>
<header class="main-content__header" role="banner">
<h1 class="main-content__page-title">
<%= content_for(:title) %>
</h1>
<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 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>
</div>
<div class="flex flex-col justify-center">
<h1 class="text-base font-medium tracking-tight text-n-slate-12">
<%= content_for(:title) %>
</h1>
<p class="text-xs text-n-slate-11 m-0">Profile, accounts, and access control</p>
</div>
</div>
<div>
<%= link_to(
<div class="flex items-center gap-2">
<% if authorized_action?(page.resource, :edit) %>
<%= link_to(
t("administrate.actions.edit"),
[:edit, namespace, page.resource.becomes(User)],
class: "button",
) if authorized_action? page.resource, :edit %>
</div>
</header>
<section class="main-content__body">
<dl>
<% page.attributes.each do |title, attributes| %>
<fieldset class="<%= "field-unit--nested" if title.present? %>">
<% if title.present? %>
<legend><%= t "helpers.label.#{page.resource_name}.#{title}", default: title %></legend>
<% end %>
<% attributes.each do |attribute| %>
<dt class="attribute-label" id="<%= attribute.name %>">
<%= t(
"helpers.label.#{resource_name}.#{attribute.name}",
default: page.resource.class.human_attribute_name(attribute.name),
[:edit, namespace, page.resource.becomes(User)],
class: "inline-flex items-center justify-center h-8 px-3 text-sm font-medium text-white bg-woot-500 rounded-lg hover:bg-woot-600 transition-colors"
) %>
</dt>
<dd class="attribute-data attribute-data--<%=attribute.html_class%>"
><%= render_field attribute, page: page %></dd>
<% end %>
</fieldset>
<% end %>
</dl>
</div>
</div>
</header>
<main class="flex-1 px-6 overflow-y-auto">
<div class="w-full mx-auto max-w-[60rem] py-6 space-y-6">
<%# User Information Card %>
<div class="rounded-xl bg-n-solid-2 shadow outline-1 outline outline-n-container overflow-hidden">
<div class="px-6 py-4 border-b border-n-weak">
<h2 class="text-base font-medium text-n-slate-12">User Information</h2>
</div>
<div class="divide-y divide-n-weak">
<% page.attributes.each do |title, attributes| %>
<% next if title.present? %>
<% attributes.each do |attribute| %>
<% next if ['account_users', 'custom_attributes'].include?(attribute.name.to_s) %>
<div class="grid grid-cols-[180px,1fr] items-center min-h-12 px-6">
<span class="text-sm text-n-slate-11">
<%= t(
"helpers.label.#{resource_name}.#{attribute.name}",
default: page.resource.class.human_attribute_name(attribute.name),
) %>
</span>
<span class="text-sm text-n-slate-12 py-3">
<%= render_field attribute, page: page %>
</span>
</div>
<% end %>
<% end %>
</div>
</div>
<%# Account Users Card %>
<% page.attributes.each do |title, attributes| %>
<% next if title.present? %>
<% attributes.each do |attribute| %>
<% if attribute.name.to_s == 'account_users' && page.resource.account_users.any? %>
<div class="rounded-xl bg-n-solid-2 shadow outline-1 outline outline-n-container overflow-hidden">
<div class="px-6 py-4 border-b border-n-weak">
<h2 class="text-base font-medium text-n-slate-12">Account Users</h2>
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-n-slate-1">
<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>
</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">
<%= 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">
<%= 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">
<% 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">
<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>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% end %>
<% end %>
<% end %>
<%# Custom Attributes Card %>
<% if page.resource.try(:custom_attributes).present? %>
<div class="rounded-xl bg-n-solid-2 shadow outline-1 outline outline-n-container overflow-hidden">
<div class="px-6 py-4 border-b border-n-weak">
<h2 class="text-base font-medium text-n-slate-12">Custom Attributes</h2>
</div>
<div class="divide-y divide-n-weak">
<% page.resource.custom_attributes.each do |key, value| %>
<div class="grid grid-cols-[180px,1fr] items-start min-h-12 px-6">
<span class="text-sm text-n-slate-11 py-3">
<%= key.to_s.titleize %>
</span>
<span class="text-sm text-n-slate-12 py-3 break-all font-mono">
<%= value %>
</span>
</div>
<% end %>
</div>
</div>
<% end %>
<%# Actions Card %>
<div class="rounded-xl bg-n-solid-2 shadow outline-1 outline outline-n-container overflow-hidden">
<div class="px-6 py-4 border-b border-n-weak">
<h2 class="text-base font-medium text-n-slate-12">Actions</h2>
</div>
<div class="divide-y divide-n-weak">
<div class="px-6 py-4">
<h3 class="text-sm font-medium text-n-slate-12 mb-3">Add User to Account</h3>
<%= render 'super_admin/shared/account_user_form', page: page, namespace: namespace, resource_type: 'user' %>
</div>
<%= render partial: "impersonate", locals: {page: page} %>
</div>
</div>
</div>
</main>
</section>
<%= render 'super_admin/shared/account_user_form', page: page, namespace: namespace, resource_type: 'user' %>
<%= render partial: "impersonate", locals: {page: page} %>