fix(ws): 发送 WebSocket 协议级 Ping 控制帧防止 60s 后连接断开
writePump 只发 ActionCable 文本 JSON ping,从不发 websocket.PingMessage, 导致 readPump 的 PongHandler 永远不触发,ReadDeadline(60s) 到期后连接 被强制关闭。前端每 60 秒弹出"离线""重连"通知。 修复:在 ticker 中先发 PingMessage 控制帧(浏览器自动回 Pong 重置 ReadDeadline),再发 ActionCable 文本 ping(保活 ConnectionMonitor)。 同时包含此前会话中的其他变更: - 移除安全设置(SAML)页面及路由 - 新增超级管理员控制台前端页面 - platform agent_bots 路由使用 Platform* handler - 超级管理员入口改为 SPA 内路由跳转
This commit is contained in:
@@ -194,7 +194,19 @@ func (h *Handler) writePump(client *Client) {
|
||||
}
|
||||
|
||||
case <-ticker.C:
|
||||
// Send ActionCable-level ping message (JSON text frame).
|
||||
// Send WebSocket protocol-level ping control frame.
|
||||
// The browser automatically responds with a Pong, which triggers
|
||||
// the PongHandler in readPump and resets the ReadDeadline.
|
||||
// Without this, the ReadDeadline (PongWait=60s) expires and the
|
||||
// connection is forcibly closed, causing the client to show
|
||||
// "offline" / "reconnecting" notifications every ~60 seconds.
|
||||
client.Conn.SetWriteDeadline(time.Now().Add(WriteWait))
|
||||
if err := client.Conn.WriteMessage(websocket.PingMessage, nil); err != nil {
|
||||
logger.L().Errorf("ws: ws-ping control frame failed for user=%d: %v", client.UserID, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Also send ActionCable-level ping message (JSON text frame).
|
||||
// The JS ConnectionMonitor expects periodic ping messages to
|
||||
// keep the connection alive (staleThreshold = 6s by default).
|
||||
pingMsg, _ := json.Marshal(PingFrame{
|
||||
@@ -203,7 +215,7 @@ func (h *Handler) writePump(client *Client) {
|
||||
})
|
||||
client.Conn.SetWriteDeadline(time.Now().Add(WriteWait))
|
||||
if err := client.Conn.WriteMessage(websocket.TextMessage, pingMsg); err != nil {
|
||||
logger.L().Errorf("ws: ping write failed for user=%d: %v", client.UserID, err)
|
||||
logger.L().Errorf("ws: actioncable ping write failed for user=%d: %v", client.UserID, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2040,11 +2040,12 @@ func registerPlatformRoutes(g *gin.RouterGroup, h *Handlers) {
|
||||
|
||||
// Agent Bot CRUD (super-admin, global bots without account_id)
|
||||
// Reference: Chatwoot namespace :agent_bots under :platform_app
|
||||
g.GET("/agent_bots", h.AgentBot.List)
|
||||
g.POST("/agent_bots", h.AgentBot.Create)
|
||||
g.GET("/agent_bots/:id", h.AgentBot.Get)
|
||||
g.PUT("/agent_bots/:id", h.AgentBot.Update)
|
||||
g.DELETE("/agent_bots/:id", h.AgentBot.Delete)
|
||||
// Use Platform* handlers — these don't require account_id from URL params.
|
||||
g.GET("/agent_bots", h.AgentBot.PlatformList)
|
||||
g.POST("/agent_bots", h.AgentBot.PlatformCreate)
|
||||
g.GET("/agent_bots/:id", h.AgentBot.PlatformGet)
|
||||
g.PUT("/agent_bots/:id", h.AgentBot.PlatformUpdate)
|
||||
g.DELETE("/agent_bots/:id", h.AgentBot.PlatformDelete)
|
||||
g.POST("/agent_bots/:id/reset_token", h.AgentBot.ResetToken)
|
||||
g.POST("/agent_bots/:id/reset_secret", h.AgentBot.ResetSecret)
|
||||
g.POST("/agent_bots/:id/delete_avatar", h.AgentBot.DeleteAvatar)
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/* global axios */
|
||||
|
||||
/**
|
||||
* Platform API client for Super Admin endpoints.
|
||||
* All endpoints are under /platform/api/v1/ and require super_admin auth.
|
||||
*/
|
||||
class PlatformAPI {
|
||||
constructor() {
|
||||
this.baseURL = '/platform/api/v1';
|
||||
}
|
||||
|
||||
// ── Accounts ──────────────────────────────────────────
|
||||
getAccounts(params = {}) {
|
||||
return axios.get(`${this.baseURL}/accounts`, { params });
|
||||
}
|
||||
|
||||
getAccount(id) {
|
||||
return axios.get(`${this.baseURL}/accounts/${id}`);
|
||||
}
|
||||
|
||||
createAccount(data) {
|
||||
return axios.post(`${this.baseURL}/accounts`, data);
|
||||
}
|
||||
|
||||
updateAccount(id, data) {
|
||||
return axios.patch(`${this.baseURL}/accounts/${id}`, data);
|
||||
}
|
||||
|
||||
deleteAccount(id) {
|
||||
return axios.delete(`${this.baseURL}/accounts/${id}`);
|
||||
}
|
||||
|
||||
// ── Users ─────────────────────────────────────────────
|
||||
getUsers(params = {}) {
|
||||
return axios.get(`${this.baseURL}/users`, { params });
|
||||
}
|
||||
|
||||
getUser(id) {
|
||||
return axios.get(`${this.baseURL}/users/${id}`);
|
||||
}
|
||||
|
||||
createUser(data) {
|
||||
return axios.post(`${this.baseURL}/users`, data);
|
||||
}
|
||||
|
||||
updateUser(id, data) {
|
||||
return axios.patch(`${this.baseURL}/users/${id}`, data);
|
||||
}
|
||||
|
||||
deleteUser(id) {
|
||||
return axios.delete(`${this.baseURL}/users/${id}`);
|
||||
}
|
||||
|
||||
// ── Account Users ─────────────────────────────────────
|
||||
getAccountUsers(accountId) {
|
||||
return axios.get(
|
||||
`${this.baseURL}/accounts/${accountId}/account_users`
|
||||
);
|
||||
}
|
||||
|
||||
createAccountUser(accountId, data) {
|
||||
return axios.post(
|
||||
`${this.baseURL}/accounts/${accountId}/account_users`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
deleteAccountUser(accountId, userId) {
|
||||
return axios.delete(
|
||||
`${this.baseURL}/accounts/${userId}/account_users`,
|
||||
{ data: { user_id: userId } }
|
||||
);
|
||||
}
|
||||
|
||||
// ── Agent Bots (platform-level) ───────────────────────
|
||||
getAgentBots() {
|
||||
return axios.get(`${this.baseURL}/agent_bots`);
|
||||
}
|
||||
|
||||
createAgentBot(data) {
|
||||
return axios.post(`${this.baseURL}/agent_bots`, data);
|
||||
}
|
||||
|
||||
updateAgentBot(id, data) {
|
||||
return axios.put(`${this.baseURL}/agent_bots/${id}`, data);
|
||||
}
|
||||
|
||||
deleteAgentBot(id) {
|
||||
return axios.delete(`${this.baseURL}/agent_bots/${id}`);
|
||||
}
|
||||
|
||||
// ── Installation Config ───────────────────────────────
|
||||
getInstallationConfigs(params = {}) {
|
||||
return axios.get(`${this.baseURL}/installation_configs`, { params });
|
||||
}
|
||||
|
||||
createInstallationConfig(data) {
|
||||
return axios.post(`${this.baseURL}/installation_configs`, data);
|
||||
}
|
||||
|
||||
updateInstallationConfig(id, data) {
|
||||
return axios.put(`${this.baseURL}/installation_configs/${id}`, data);
|
||||
}
|
||||
|
||||
deleteInstallationConfig(id) {
|
||||
return axios.delete(`${this.baseURL}/installation_configs/${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new PlatformAPI();
|
||||
@@ -783,12 +783,6 @@ const menuItems = computed(() => {
|
||||
icon: 'i-lucide-workflow',
|
||||
to: accountScopedRoute('conversation_workflow_index'),
|
||||
},
|
||||
{
|
||||
name: 'Settings Security',
|
||||
label: t('SIDEBAR.SECURITY'),
|
||||
icon: 'i-lucide-shield',
|
||||
to: accountScopedRoute('security_settings_index'),
|
||||
},
|
||||
{
|
||||
name: 'Settings Billing',
|
||||
label: t('SIDEBAR.BILLING'),
|
||||
|
||||
@@ -104,9 +104,7 @@ const menuItems = computed(() => {
|
||||
showOnCustomBrandedInstance: true,
|
||||
label: t('SIDEBAR_ITEMS.SUPER_ADMIN_CONSOLE'),
|
||||
icon: 'i-lucide-castle',
|
||||
link: '/super_admin',
|
||||
nativeLink: true,
|
||||
target: '_blank',
|
||||
link: { name: 'super_admin_dashboard' },
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
|
||||
@@ -35,6 +35,7 @@ import settings from './settings.json';
|
||||
import signup from './signup.json';
|
||||
import sla from './sla.json';
|
||||
import snooze from './snooze.json';
|
||||
import superAdmin from './superAdmin.json';
|
||||
import teamsSettings from './teamsSettings.json';
|
||||
import whatsappTemplates from './whatsappTemplates.json';
|
||||
import contentTemplates from './contentTemplates.json';
|
||||
@@ -79,6 +80,7 @@ export default {
|
||||
...signup,
|
||||
...sla,
|
||||
...snooze,
|
||||
...superAdmin,
|
||||
...teamsSettings,
|
||||
...whatsappTemplates,
|
||||
...contentTemplates,
|
||||
|
||||
@@ -381,7 +381,6 @@
|
||||
"INFO_SHORT": "Automatically mark offline when you aren't using the app."
|
||||
},
|
||||
"DOCS": "Read docs",
|
||||
"SECURITY": "Security",
|
||||
"CAPTAIN_AI": "Copilot Configuration",
|
||||
"CONVERSATION_WORKFLOW": "Conversation Workflow"
|
||||
},
|
||||
@@ -568,78 +567,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SECURITY_SETTINGS": {
|
||||
"TITLE": "Security",
|
||||
"DESCRIPTION": "Manage your account security settings.",
|
||||
"LINK_TEXT": "Learn more about SAML SSO",
|
||||
"SAML_DISABLED_MESSAGE": "SAML SSO is currently disabled. Please contact your administrator to enable this feature.",
|
||||
"SAML": {
|
||||
"TITLE": "SAML SSO",
|
||||
"NOTE": "Configure SAML single sign-on for your account. Users will authenticate through your identity provider instead of using email/password.",
|
||||
"ACS_URL": {
|
||||
"LABEL": "ACS URL",
|
||||
"TOOLTIP": "Assertion Consumer Service URL - Configure this URL in your IdP as the destination for SAML responses"
|
||||
},
|
||||
"SSO_URL": {
|
||||
"LABEL": "SSO URL",
|
||||
"HELP": "The URL where SAML authentication requests will be sent",
|
||||
"PLACEHOLDER": "https://your-idp.com/saml/sso"
|
||||
},
|
||||
"CERTIFICATE": {
|
||||
"LABEL": "Signing certificate in PEM format",
|
||||
"HELP": "The public certificate from your identity provider used to verify SAML responses",
|
||||
"PLACEHOLDER": "-----BEGIN CERTIFICATE-----\nMIIC..."
|
||||
},
|
||||
"FINGERPRINT": {
|
||||
"LABEL": "Fingerprint",
|
||||
"TOOLTIP": "SHA-1 fingerprint of the certificate - Use this to verify the certificate in your IdP configuration"
|
||||
},
|
||||
"COPY_SUCCESS": "Copied to clipboard",
|
||||
"SP_ENTITY_ID": {
|
||||
"LABEL": "SP Entity ID",
|
||||
"HELP": "Unique identifier for this application as a service provider (auto-generated).",
|
||||
"TOOLTIP": "Unique identifier for Chatwoot as the Service Provider - Configure this in your IdP settings"
|
||||
},
|
||||
"IDP_ENTITY_ID": {
|
||||
"LABEL": "Identity Provider Entity ID",
|
||||
"HELP": "Unique identifier for your identity provider (usually found in IdP configuration)",
|
||||
"PLACEHOLDER": "https://your-idp.com/saml"
|
||||
},
|
||||
"UPDATE_BUTTON": "Update SAML Settings",
|
||||
"API": {
|
||||
"SUCCESS": "SAML settings updated successfully",
|
||||
"ERROR": "Failed to update SAML settings",
|
||||
"ERROR_LOADING": "Failed to load SAML settings",
|
||||
"DISABLED": "SAML settings disabled successfully"
|
||||
},
|
||||
"VALIDATION": {
|
||||
"REQUIRED_FIELDS": "SSO URL, Identity Provider Entity ID, and Certificate are required fields",
|
||||
"SSO_URL_ERROR": "Please enter a valid SSO URL",
|
||||
"CERTIFICATE_ERROR": "Certificate is required",
|
||||
"IDP_ENTITY_ID_ERROR": "Identity Provider Entity ID is required"
|
||||
},
|
||||
"ENTERPRISE_PAYWALL": {
|
||||
"AVAILABLE_ON": "The SAML SSO feature is only available in the Enterprise plans.",
|
||||
"UPGRADE_PROMPT": "Upgrade to an Enterprise plan to access SAML single sign-on and other advanced security features.",
|
||||
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
|
||||
},
|
||||
"PAYWALL": {
|
||||
"TITLE": "Upgrade to enable SAML SSO",
|
||||
"AVAILABLE_ON": "The SAML SSO feature is only available in the Enterprise plans.",
|
||||
"UPGRADE_PROMPT": "Upgrade your plan to get access to SAML single sign-on and other advanced features.",
|
||||
"UPGRADE_NOW": "Upgrade now",
|
||||
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
|
||||
},
|
||||
"ATTRIBUTE_MAPPING": {
|
||||
"TITLE": "SAML Attribute Setup",
|
||||
"DESCRIPTION": "The following attribute mappings must be configured in your identity provider"
|
||||
},
|
||||
"INFO_SECTION": {
|
||||
"TITLE": "Service Provider Information",
|
||||
"TOOLTIP": "Copy these values and configure them in your Identity Provider to establish the SAML connection"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CONVERSATION_WORKFLOW": {
|
||||
"INDEX": {
|
||||
"HEADER": {
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
{
|
||||
"SUPER_ADMIN": {
|
||||
"SIDEBAR": {
|
||||
"TITLE": "Super Admin",
|
||||
"DASHBOARD": "Dashboard",
|
||||
"ACCOUNTS": "Accounts",
|
||||
"USERS": "Users",
|
||||
"AGENT_BOTS": "Agent Bots",
|
||||
"CONFIGS": "Installation Config",
|
||||
"BACK_TO_DASHBOARD": "Back to Dashboard"
|
||||
},
|
||||
"NO_ACCESS": {
|
||||
"TITLE": "Access Denied",
|
||||
"MESSAGE": "This page is only accessible to super admins."
|
||||
},
|
||||
"COMMON": {
|
||||
"EDIT": "Edit",
|
||||
"DELETE": "Delete",
|
||||
"CONFIRM_DELETE": "Confirm Delete",
|
||||
"CANCEL": "Cancel"
|
||||
},
|
||||
"DASHBOARD": {
|
||||
"TITLE": "Platform Dashboard",
|
||||
"DESCRIPTION": "Platform-wide statistics and overview",
|
||||
"STATS": {
|
||||
"ACCOUNTS": "Total Accounts",
|
||||
"USERS": "Total Users"
|
||||
},
|
||||
"RECENT_ACCOUNTS": "Recent Accounts"
|
||||
},
|
||||
"ACCOUNTS": {
|
||||
"TITLE": "Account Management",
|
||||
"DESCRIPTION": "Manage all accounts on the platform",
|
||||
"SEARCH": "Search accounts...",
|
||||
"EMPTY": "No accounts found",
|
||||
"TABLE": {
|
||||
"NAME": "Name",
|
||||
"STATUS": "Status",
|
||||
"ACTIONS": "Actions"
|
||||
},
|
||||
"CREATE": {
|
||||
"TITLE": "Create Account",
|
||||
"SUCCESS": "Account created successfully"
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "Edit Account",
|
||||
"SUCCESS": "Account updated successfully"
|
||||
},
|
||||
"DELETE": {
|
||||
"TITLE": "Delete Account",
|
||||
"MESSAGE": "Are you sure you want to delete account \"{name}\"? This action cannot be undone.",
|
||||
"SUCCESS": "Account deleted successfully"
|
||||
},
|
||||
"FIELDS": {
|
||||
"NAME": "Account Name",
|
||||
"NAME_PLACEHOLDER": "Enter account name"
|
||||
},
|
||||
"ERROR": "Operation failed, please try again"
|
||||
},
|
||||
"USERS": {
|
||||
"TITLE": "User Management",
|
||||
"DESCRIPTION": "Manage all users on the platform",
|
||||
"SEARCH": "Search users...",
|
||||
"EMPTY": "No users found",
|
||||
"TABLE": {
|
||||
"NAME": "User",
|
||||
"TYPE": "Type",
|
||||
"ACTIONS": "Actions"
|
||||
},
|
||||
"CREATE": {
|
||||
"TITLE": "Create User",
|
||||
"SUCCESS": "User created successfully"
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "Edit User",
|
||||
"SUCCESS": "User updated successfully"
|
||||
},
|
||||
"DELETE": {
|
||||
"TITLE": "Delete User",
|
||||
"MESSAGE": "Are you sure you want to delete user \"{name}\"? This action cannot be undone.",
|
||||
"SUCCESS": "User deleted successfully"
|
||||
},
|
||||
"FIELDS": {
|
||||
"NAME": "Name",
|
||||
"NAME_PLACEHOLDER": "Enter user name",
|
||||
"EMAIL": "Email",
|
||||
"PASSWORD": "Password",
|
||||
"PASSWORD_HINT": "Leave empty to keep current password",
|
||||
"TYPE": "User Type",
|
||||
"TYPE_USER": "Regular User",
|
||||
"TYPE_SUPER_ADMIN": "Super Admin",
|
||||
"TYPE_HINT": "Set to SuperAdmin to grant super admin privileges"
|
||||
},
|
||||
"ERROR": "Operation failed, please try again"
|
||||
},
|
||||
"AGENT_BOTS": {
|
||||
"TITLE": "Agent Bot Management",
|
||||
"DESCRIPTION": "Manage platform-level agent bots",
|
||||
"SEARCH": "Search agent bots...",
|
||||
"EMPTY": "No agent bots found",
|
||||
"GLOBAL_BADGE": "System Bot",
|
||||
"TABLE": {
|
||||
"NAME": "Bot",
|
||||
"URL": "Webhook URL",
|
||||
"ACTIONS": "Actions"
|
||||
},
|
||||
"CREATE": {
|
||||
"TITLE": "Create Agent Bot",
|
||||
"SUCCESS": "Agent bot created successfully"
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "Edit Agent Bot",
|
||||
"SUCCESS": "Agent bot updated successfully"
|
||||
},
|
||||
"DELETE": {
|
||||
"TITLE": "Delete Agent Bot",
|
||||
"MESSAGE": "Are you sure you want to delete agent bot \"{name}\"?",
|
||||
"SUCCESS": "Agent bot deleted successfully"
|
||||
},
|
||||
"FIELDS": {
|
||||
"NAME": "Name",
|
||||
"DESCRIPTION": "Description",
|
||||
"DESCRIPTION_PLACEHOLDER": "Enter bot description",
|
||||
"URL": "Webhook URL",
|
||||
"TYPE": "Type",
|
||||
"TYPE_HINT": "Defaults to webhook"
|
||||
},
|
||||
"ERROR": "Operation failed, please try again"
|
||||
},
|
||||
"CONFIGS": {
|
||||
"TITLE": "Installation Config",
|
||||
"DESCRIPTION": "Manage platform-wide key-value configuration",
|
||||
"SEARCH": "Search configs...",
|
||||
"EMPTY": "No configuration items found",
|
||||
"TABLE": {
|
||||
"NAME": "Name",
|
||||
"VALUE": "Value",
|
||||
"ACTIONS": "Actions"
|
||||
},
|
||||
"CREATE": {
|
||||
"TITLE": "Create Config",
|
||||
"SUCCESS": "Configuration created successfully"
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "Edit Config",
|
||||
"SUCCESS": "Configuration updated successfully"
|
||||
},
|
||||
"DELETE": {
|
||||
"TITLE": "Delete Config",
|
||||
"MESSAGE": "Are you sure you want to delete config \"{name}\"?",
|
||||
"SUCCESS": "Configuration deleted successfully"
|
||||
},
|
||||
"FIELDS": {
|
||||
"NAME": "Config Name",
|
||||
"NAME_HINT": "Use uppercase snake_case, e.g. INSTALLATION_NAME",
|
||||
"VALUE": "Config Value",
|
||||
"VALUE_PLACEHOLDER": "Enter config value",
|
||||
"LOCKED": "Locked",
|
||||
"LOCKED_HINT": "When locked, non-super-admins cannot modify"
|
||||
},
|
||||
"ERROR": "Operation failed, please try again"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ import setNewPassword from './setNewPassword.json';
|
||||
import settings from './settings.json';
|
||||
import signup from './signup.json';
|
||||
import sla from './sla.json';
|
||||
import superAdmin from './superAdmin.json';
|
||||
import teamsSettings from './teamsSettings.json';
|
||||
import whatsappTemplates from './whatsappTemplates.json';
|
||||
|
||||
@@ -76,6 +77,7 @@ export default {
|
||||
...settings,
|
||||
...signup,
|
||||
...sla,
|
||||
...superAdmin,
|
||||
...teamsSettings,
|
||||
...whatsappTemplates,
|
||||
};
|
||||
|
||||
@@ -381,7 +381,6 @@
|
||||
"INFO_SHORT": "当您不使用应用程序时自动标记离线。"
|
||||
},
|
||||
"DOCS": "阅读文档",
|
||||
"SECURITY": "安全",
|
||||
"CAPTAIN_AI": "Copilot 配置",
|
||||
"CONVERSATION_WORKFLOW": "会话工作流"
|
||||
},
|
||||
@@ -568,78 +567,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SECURITY_SETTINGS": {
|
||||
"TITLE": "安全",
|
||||
"DESCRIPTION": "管理您的账户安全设置。",
|
||||
"LINK_TEXT": "了解更多关于 SAML SSO 的信息",
|
||||
"SAML_DISABLED_MESSAGE": "SAML SSO is currently disabled. Please contact your administrator to enable this feature.",
|
||||
"SAML": {
|
||||
"TITLE": "SAML SSO",
|
||||
"NOTE": "Configure SAML single sign-on for your account. Users will authenticate through your identity provider instead of using email/password.",
|
||||
"ACS_URL": {
|
||||
"LABEL": "ACS URL",
|
||||
"TOOLTIP": "Assertion Consumer Service URL - Configure this URL in your IdP as the destination for SAML responses"
|
||||
},
|
||||
"SSO_URL": {
|
||||
"LABEL": "SSO URL",
|
||||
"HELP": "The URL where SAML authentication requests will be sent",
|
||||
"PLACEHOLDER": "https://your-idp.com/saml/sso"
|
||||
},
|
||||
"CERTIFICATE": {
|
||||
"LABEL": "Signing certificate in PEM format",
|
||||
"HELP": "The public certificate from your identity provider used to verify SAML responses",
|
||||
"PLACEHOLDER": "-----BEGIN CERTIFICATE-----\nMIIC..."
|
||||
},
|
||||
"FINGERPRINT": {
|
||||
"LABEL": "Fingerprint",
|
||||
"TOOLTIP": "SHA-1 fingerprint of the certificate - Use this to verify the certificate in your IdP configuration"
|
||||
},
|
||||
"COPY_SUCCESS": "已复制到剪贴板",
|
||||
"SP_ENTITY_ID": {
|
||||
"LABEL": "SP Entity ID",
|
||||
"HELP": "Unique identifier for this application as a service provider (auto-generated).",
|
||||
"TOOLTIP": "Unique identifier for Chatwoot as the Service Provider - Configure this in your IdP settings"
|
||||
},
|
||||
"IDP_ENTITY_ID": {
|
||||
"LABEL": "Identity Provider Entity ID",
|
||||
"HELP": "Unique identifier for your identity provider (usually found in IdP configuration)",
|
||||
"PLACEHOLDER": "https://your-idp.com/saml"
|
||||
},
|
||||
"UPDATE_BUTTON": "更新 SAML 设置",
|
||||
"API": {
|
||||
"SUCCESS": "SAML settings updated successfully",
|
||||
"ERROR": "Failed to update SAML settings",
|
||||
"ERROR_LOADING": "Failed to load SAML settings",
|
||||
"DISABLED": "SAML settings disabled successfully"
|
||||
},
|
||||
"VALIDATION": {
|
||||
"REQUIRED_FIELDS": "SSO URL, Identity Provider Entity ID, and Certificate are required fields",
|
||||
"SSO_URL_ERROR": "Please enter a valid SSO URL",
|
||||
"CERTIFICATE_ERROR": "Certificate is required",
|
||||
"IDP_ENTITY_ID_ERROR": "Identity Provider Entity ID is required"
|
||||
},
|
||||
"ENTERPRISE_PAYWALL": {
|
||||
"AVAILABLE_ON": "The SAML SSO feature is only available in the Enterprise plans.",
|
||||
"UPGRADE_PROMPT": "Upgrade to an Enterprise plan to access SAML single sign-on and other advanced security features.",
|
||||
"ASK_ADMIN": "请联系您的管理员进行升级。"
|
||||
},
|
||||
"PAYWALL": {
|
||||
"TITLE": "Upgrade to enable SAML SSO",
|
||||
"AVAILABLE_ON": "The SAML SSO feature is only available in the Enterprise plans.",
|
||||
"UPGRADE_PROMPT": "Upgrade your plan to get access to SAML single sign-on and other advanced features.",
|
||||
"UPGRADE_NOW": "立即升级",
|
||||
"CANCEL_ANYTIME": "您可以随时更改或取消您的计划"
|
||||
},
|
||||
"ATTRIBUTE_MAPPING": {
|
||||
"TITLE": "SAML Attribute Setup",
|
||||
"DESCRIPTION": "The following attribute mappings must be configured in your identity provider"
|
||||
},
|
||||
"INFO_SECTION": {
|
||||
"TITLE": "Service Provider Information",
|
||||
"TOOLTIP": "Copy these values and configure them in your Identity Provider to establish the SAML connection"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CONVERSATION_WORKFLOW": {
|
||||
"INDEX": {
|
||||
"HEADER": {
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
{
|
||||
"SUPER_ADMIN": {
|
||||
"SIDEBAR": {
|
||||
"TITLE": "超级管理控制台",
|
||||
"DASHBOARD": "仪表盘",
|
||||
"ACCOUNTS": "账户管理",
|
||||
"USERS": "用户管理",
|
||||
"AGENT_BOTS": "机器人管理",
|
||||
"CONFIGS": "安装配置",
|
||||
"BACK_TO_DASHBOARD": "返回工作台"
|
||||
},
|
||||
"NO_ACCESS": {
|
||||
"TITLE": "无权访问",
|
||||
"MESSAGE": "此页面仅限超级管理员访问。"
|
||||
},
|
||||
"COMMON": {
|
||||
"EDIT": "编辑",
|
||||
"DELETE": "删除",
|
||||
"CONFIRM_DELETE": "确认删除",
|
||||
"CANCEL": "取消"
|
||||
},
|
||||
"DASHBOARD": {
|
||||
"TITLE": "平台仪表盘",
|
||||
"DESCRIPTION": "平台全局统计与概览",
|
||||
"STATS": {
|
||||
"ACCOUNTS": "账户总数",
|
||||
"USERS": "用户总数"
|
||||
},
|
||||
"RECENT_ACCOUNTS": "最近账户"
|
||||
},
|
||||
"ACCOUNTS": {
|
||||
"TITLE": "账户管理",
|
||||
"DESCRIPTION": "管理平台上的所有账户",
|
||||
"SEARCH": "搜索账户...",
|
||||
"EMPTY": "暂无账户",
|
||||
"TABLE": {
|
||||
"NAME": "账户名称",
|
||||
"STATUS": "状态",
|
||||
"ACTIONS": "操作"
|
||||
},
|
||||
"CREATE": {
|
||||
"TITLE": "创建账户",
|
||||
"SUCCESS": "账户创建成功"
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "编辑账户",
|
||||
"SUCCESS": "账户更新成功"
|
||||
},
|
||||
"DELETE": {
|
||||
"TITLE": "删除账户",
|
||||
"MESSAGE": "确定要删除账户「{name}」吗?此操作不可撤销。",
|
||||
"SUCCESS": "账户删除成功"
|
||||
},
|
||||
"FIELDS": {
|
||||
"NAME": "账户名称",
|
||||
"NAME_PLACEHOLDER": "输入账户名称"
|
||||
},
|
||||
"ERROR": "操作失败,请重试"
|
||||
},
|
||||
"USERS": {
|
||||
"TITLE": "用户管理",
|
||||
"DESCRIPTION": "管理平台上的所有用户",
|
||||
"SEARCH": "搜索用户...",
|
||||
"EMPTY": "暂无用户",
|
||||
"TABLE": {
|
||||
"NAME": "用户",
|
||||
"TYPE": "类型",
|
||||
"ACTIONS": "操作"
|
||||
},
|
||||
"CREATE": {
|
||||
"TITLE": "创建用户",
|
||||
"SUCCESS": "用户创建成功"
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "编辑用户",
|
||||
"SUCCESS": "用户更新成功"
|
||||
},
|
||||
"DELETE": {
|
||||
"TITLE": "删除用户",
|
||||
"MESSAGE": "确定要删除用户「{name}」吗?此操作不可撤销。",
|
||||
"SUCCESS": "用户删除成功"
|
||||
},
|
||||
"FIELDS": {
|
||||
"NAME": "姓名",
|
||||
"NAME_PLACEHOLDER": "输入用户姓名",
|
||||
"EMAIL": "邮箱",
|
||||
"PASSWORD": "密码",
|
||||
"PASSWORD_HINT": "留空则不修改密码",
|
||||
"TYPE": "用户类型",
|
||||
"TYPE_USER": "普通用户",
|
||||
"TYPE_SUPER_ADMIN": "超级管理员",
|
||||
"TYPE_HINT": "设置为 SuperAdmin 可授予超级管理员权限"
|
||||
},
|
||||
"ERROR": "操作失败,请重试"
|
||||
},
|
||||
"AGENT_BOTS": {
|
||||
"TITLE": "机器人管理",
|
||||
"DESCRIPTION": "管理平台级 Agent Bot",
|
||||
"SEARCH": "搜索机器人...",
|
||||
"EMPTY": "暂无机器人",
|
||||
"GLOBAL_BADGE": "系统机器人",
|
||||
"TABLE": {
|
||||
"NAME": "机器人",
|
||||
"URL": "Webhook URL",
|
||||
"ACTIONS": "操作"
|
||||
},
|
||||
"CREATE": {
|
||||
"TITLE": "创建机器人",
|
||||
"SUCCESS": "机器人创建成功"
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "编辑机器人",
|
||||
"SUCCESS": "机器人更新成功"
|
||||
},
|
||||
"DELETE": {
|
||||
"TITLE": "删除机器人",
|
||||
"MESSAGE": "确定要删除机器人「{name}」吗?",
|
||||
"SUCCESS": "机器人删除成功"
|
||||
},
|
||||
"FIELDS": {
|
||||
"NAME": "名称",
|
||||
"DESCRIPTION": "描述",
|
||||
"DESCRIPTION_PLACEHOLDER": "输入机器人描述",
|
||||
"URL": "Webhook URL",
|
||||
"TYPE": "类型",
|
||||
"TYPE_HINT": "默认为 webhook"
|
||||
},
|
||||
"ERROR": "操作失败,请重试"
|
||||
},
|
||||
"CONFIGS": {
|
||||
"TITLE": "安装配置",
|
||||
"DESCRIPTION": "管理平台全局键值配置",
|
||||
"SEARCH": "搜索配置...",
|
||||
"EMPTY": "暂无配置项",
|
||||
"TABLE": {
|
||||
"NAME": "配置名称",
|
||||
"VALUE": "值",
|
||||
"ACTIONS": "操作"
|
||||
},
|
||||
"CREATE": {
|
||||
"TITLE": "创建配置",
|
||||
"SUCCESS": "配置创建成功"
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "编辑配置",
|
||||
"SUCCESS": "配置更新成功"
|
||||
},
|
||||
"DELETE": {
|
||||
"TITLE": "删除配置",
|
||||
"MESSAGE": "确定要删除配置「{name}」吗?",
|
||||
"SUCCESS": "配置删除成功"
|
||||
},
|
||||
"FIELDS": {
|
||||
"NAME": "配置名称",
|
||||
"NAME_HINT": "使用大写下划线格式,如 INSTALLATION_NAME",
|
||||
"VALUE": "配置值",
|
||||
"VALUE_PLACEHOLDER": "输入配置值",
|
||||
"LOCKED": "锁定",
|
||||
"LOCKED_HINT": "锁定后非超级管理员不可修改"
|
||||
},
|
||||
"ERROR": "操作失败,请重试"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { frontendURL } from '../../helper/URLHelper';
|
||||
import helpcenterRoutes from './helpcenter/helpcenter.routes';
|
||||
import campaignsRoutes from './campaigns/campaigns.routes';
|
||||
import { routes as captainRoutes } from './captain/captain.routes';
|
||||
import { routes as superAdminRoutes } from './super_admin/superAdmin.routes';
|
||||
import AppContainer from './Dashboard.vue';
|
||||
import Suspended from './suspended/Index.vue';
|
||||
import NoAccounts from './noAccounts/Index.vue';
|
||||
@@ -47,6 +48,7 @@ export default {
|
||||
...notificationRoutes,
|
||||
...helpcenterRoutes.routes,
|
||||
...campaignsRoutes.routes,
|
||||
...superAdminRoutes,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
import SettingsWrapper from '../SettingsWrapper.vue';
|
||||
|
||||
const SecurityIndex = {
|
||||
render() {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/settings/security'),
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: SettingsWrapper,
|
||||
props: {
|
||||
headerTitle: 'SECURITY_SETTINGS.TITLE',
|
||||
icon: 'i-lucide-shield',
|
||||
showNewButton: false,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'security_settings_index',
|
||||
component: SecurityIndex,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -23,7 +23,6 @@ import sla from './sla/sla.routes';
|
||||
import teams from './teams/teams.routes';
|
||||
import customRoles from './customRoles/customRole.routes';
|
||||
import profile from './profile/profile.routes';
|
||||
import security from './security/security.routes';
|
||||
import conversationWorkflow from './conversationWorkflow/conversationWorkflow.routes';
|
||||
import captain from './captain/captain.routes';
|
||||
|
||||
@@ -64,7 +63,6 @@ export default {
|
||||
...teams.routes,
|
||||
...customRoles.routes,
|
||||
...profile.routes,
|
||||
...security.routes,
|
||||
...conversationWorkflow.routes,
|
||||
...captain.routes,
|
||||
],
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { isSuperAdminUser } from '../settings/captain/utils';
|
||||
|
||||
import Logo from 'next/icon/Logo.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const currentUser = useMapGetter('getCurrentUser');
|
||||
const accountId = useMapGetter('getCurrentAccountId');
|
||||
|
||||
const isSuperAdmin = computed(() => isSuperAdminUser(currentUser.value));
|
||||
|
||||
const navItems = computed(() => [
|
||||
{
|
||||
name: 'super_admin_dashboard',
|
||||
label: t('SUPER_ADMIN.SIDEBAR.DASHBOARD'),
|
||||
icon: 'i-lucide-layout-dashboard',
|
||||
},
|
||||
{
|
||||
name: 'super_admin_accounts',
|
||||
label: t('SUPER_ADMIN.SIDEBAR.ACCOUNTS'),
|
||||
icon: 'i-lucide-building-2',
|
||||
},
|
||||
{
|
||||
name: 'super_admin_users',
|
||||
label: t('SUPER_ADMIN.SIDEBAR.USERS'),
|
||||
icon: 'i-lucide-users',
|
||||
},
|
||||
{
|
||||
name: 'super_admin_agent_bots',
|
||||
label: t('SUPER_ADMIN.SIDEBAR.AGENT_BOTS'),
|
||||
icon: 'i-lucide-bot',
|
||||
},
|
||||
{
|
||||
name: 'super_admin_configs',
|
||||
label: t('SUPER_ADMIN.SIDEBAR.CONFIGS'),
|
||||
icon: 'i-lucide-settings-2',
|
||||
},
|
||||
]);
|
||||
|
||||
const navigateTo = name => {
|
||||
router.push({ name, params: { accountId: accountId.value } });
|
||||
};
|
||||
|
||||
const backToDashboard = () => {
|
||||
router.push({
|
||||
name: 'home',
|
||||
params: { accountId: accountId.value },
|
||||
});
|
||||
};
|
||||
|
||||
const isActive = name => {
|
||||
return route.name === name;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="isSuperAdmin"
|
||||
class="flex w-full h-full overflow-hidden bg-n-surface-1"
|
||||
>
|
||||
<!-- Super Admin Sidebar -->
|
||||
<aside
|
||||
class="flex flex-col flex-shrink-0 w-56 h-full bg-n-background border-r border-n-weak"
|
||||
>
|
||||
<!-- Logo -->
|
||||
<div class="flex items-center gap-2 px-4 h-14 border-b border-n-weak">
|
||||
<div class="grid place-content-center size-6">
|
||||
<Logo class="size-4" />
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-n-slate-12 truncate">
|
||||
{{ t('SUPER_ADMIN.SIDEBAR.TITLE') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Nav -->
|
||||
<nav class="flex-1 px-2 py-3 overflow-y-auto no-scrollbar">
|
||||
<ul class="flex flex-col gap-0.5 m-0 list-none">
|
||||
<li v-for="item in navItems" :key="item.name">
|
||||
<button
|
||||
class="flex items-center gap-2.5 w-full px-3 py-2 rounded-lg text-sm transition-colors duration-100"
|
||||
:class="
|
||||
isActive(item.name)
|
||||
? 'bg-n-alpha-2 text-n-slate-12 font-medium'
|
||||
: 'text-n-slate-11 hover:bg-n-alpha-1 hover:text-n-slate-12'
|
||||
"
|
||||
@click="navigateTo(item.name)"
|
||||
>
|
||||
<span class="text-base flex-shrink-0" :class="item.icon" />
|
||||
<span class="truncate">{{ item.label }}</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- Back to Dashboard -->
|
||||
<div class="p-2 border-t border-n-weak">
|
||||
<Button
|
||||
:label="t('SUPER_ADMIN.SIDEBAR.BACK_TO_DASHBOARD')"
|
||||
icon="i-lucide-arrow-left"
|
||||
variant="ghost"
|
||||
color="slate"
|
||||
size="sm"
|
||||
class="w-full"
|
||||
@click="backToDashboard"
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Page Content -->
|
||||
<main class="flex-1 overflow-y-auto">
|
||||
<router-view />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Not super admin -->
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-col items-center justify-center w-full h-full gap-2 bg-n-surface-1"
|
||||
>
|
||||
<span class="i-lucide-shield-x size-12 text-n-slate-10" />
|
||||
<p class="text-lg font-medium text-n-slate-12">
|
||||
{{ t('SUPER_ADMIN.NO_ACCESS.TITLE') }}
|
||||
</p>
|
||||
<p class="text-sm text-n-slate-11">
|
||||
{{ t('SUPER_ADMIN.NO_ACCESS.MESSAGE') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,236 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { picoSearch } from '@scmmishra/pico-search';
|
||||
|
||||
import PageHeader from '../components/PageHeader.vue';
|
||||
import FormDialog from '../components/FormDialog.vue';
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||
import {
|
||||
BaseTable,
|
||||
BaseTableRow,
|
||||
BaseTableCell,
|
||||
} from 'dashboard/components-next/table';
|
||||
|
||||
const MODAL_TYPES = { CREATE: 'create', EDIT: 'edit' };
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const accounts = useMapGetter('platform/getAccounts');
|
||||
const uiFlags = useMapGetter('platform/getUIFlags');
|
||||
|
||||
const searchQuery = ref('');
|
||||
const modalType = ref(MODAL_TYPES.CREATE);
|
||||
const selectedAccount = ref({});
|
||||
const formDialogRef = ref(null);
|
||||
const deleteDialogRef = ref(null);
|
||||
const loading = ref({});
|
||||
|
||||
const isLoading = computed(() => uiFlags.value?.isFetchingAccounts);
|
||||
|
||||
const filteredAccounts = computed(() => {
|
||||
const query = searchQuery.value.trim();
|
||||
if (!query) return accounts.value;
|
||||
return picoSearch(accounts.value, query, ['name']);
|
||||
});
|
||||
|
||||
const dialogTitle = computed(() =>
|
||||
modalType.value === MODAL_TYPES.CREATE
|
||||
? t('SUPER_ADMIN.ACCOUNTS.CREATE.TITLE')
|
||||
: t('SUPER_ADMIN.ACCOUNTS.EDIT.TITLE')
|
||||
);
|
||||
|
||||
const formFields = computed(() => [
|
||||
{
|
||||
key: 'name',
|
||||
label: t('SUPER_ADMIN.ACCOUNTS.FIELDS.NAME'),
|
||||
placeholder: t('SUPER_ADMIN.ACCOUNTS.FIELDS.NAME_PLACEHOLDER'),
|
||||
required: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const openCreateModal = () => {
|
||||
modalType.value = MODAL_TYPES.CREATE;
|
||||
selectedAccount.value = {};
|
||||
formDialogRef.value?.open();
|
||||
};
|
||||
|
||||
const openEditModal = account => {
|
||||
modalType.value = MODAL_TYPES.EDIT;
|
||||
selectedAccount.value = { ...account };
|
||||
formDialogRef.value?.open();
|
||||
};
|
||||
|
||||
const openDeleteDialog = account => {
|
||||
selectedAccount.value = account;
|
||||
deleteDialogRef.value?.open();
|
||||
};
|
||||
|
||||
const handleConfirm = async data => {
|
||||
try {
|
||||
if (modalType.value === MODAL_TYPES.CREATE) {
|
||||
await store.dispatch('platform/createAccount', data);
|
||||
useAlert(t('SUPER_ADMIN.ACCOUNTS.CREATE.SUCCESS'));
|
||||
} else {
|
||||
await store.dispatch('platform/updateAccount', {
|
||||
id: selectedAccount.value.id,
|
||||
data,
|
||||
});
|
||||
useAlert(t('SUPER_ADMIN.ACCOUNTS.EDIT.SUCCESS'));
|
||||
}
|
||||
formDialogRef.value?.close();
|
||||
} catch (error) {
|
||||
useAlert(t('SUPER_ADMIN.ACCOUNTS.ERROR'));
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDelete = async () => {
|
||||
loading.value[selectedAccount.value.id] = true;
|
||||
try {
|
||||
await store.dispatch('platform/deleteAccount', selectedAccount.value.id);
|
||||
useAlert(t('SUPER_ADMIN.ACCOUNTS.DELETE.SUCCESS'));
|
||||
} catch (error) {
|
||||
useAlert(t('SUPER_ADMIN.ACCOUNTS.ERROR'));
|
||||
} finally {
|
||||
loading.value[selectedAccount.value.id] = false;
|
||||
deleteDialogRef.value?.close();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('platform/fetchAccounts');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageHeader
|
||||
:title="t('SUPER_ADMIN.ACCOUNTS.TITLE')"
|
||||
:description="t('SUPER_ADMIN.ACCOUNTS.DESCRIPTION')"
|
||||
v-model:search-query="searchQuery"
|
||||
:search-placeholder="t('SUPER_ADMIN.ACCOUNTS.SEARCH')"
|
||||
:button-label="t('SUPER_ADMIN.ACCOUNTS.CREATE.TITLE')"
|
||||
:count="accounts?.length"
|
||||
@click="openCreateModal"
|
||||
/>
|
||||
<div class="flex-1 px-6">
|
||||
<div class="w-full max-w-5xl mx-auto py-6">
|
||||
<div v-if="isLoading" class="flex items-center justify-center py-20">
|
||||
<Spinner />
|
||||
</div>
|
||||
<div
|
||||
v-else-if="filteredAccounts?.length"
|
||||
class="flex flex-col rounded-xl bg-n-background border border-n-weak overflow-hidden"
|
||||
>
|
||||
<BaseTable
|
||||
:headers="[
|
||||
t('SUPER_ADMIN.ACCOUNTS.TABLE.NAME'),
|
||||
t('SUPER_ADMIN.ACCOUNTS.TABLE.STATUS'),
|
||||
t('SUPER_ADMIN.ACCOUNTS.TABLE.ACTIONS'),
|
||||
]"
|
||||
:items="filteredAccounts"
|
||||
>
|
||||
<template #row="{ items }">
|
||||
<BaseTableRow
|
||||
v-for="account in items"
|
||||
:key="account.id"
|
||||
:item="account"
|
||||
>
|
||||
<template #default>
|
||||
<BaseTableCell>
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<Avatar
|
||||
:name="account.name"
|
||||
:size="36"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
<div class="min-w-0">
|
||||
<span class="text-sm text-n-slate-12 truncate block">
|
||||
{{ account.name }}
|
||||
</span>
|
||||
<span class="text-xs text-n-slate-10 truncate block">
|
||||
ID: {{ account.id }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
<BaseTableCell>
|
||||
<span
|
||||
class="text-xs px-2 py-0.5 rounded-md"
|
||||
:class="
|
||||
account.status === 'active'
|
||||
? 'bg-n-green-3 text-n-green-11'
|
||||
: 'bg-n-amber-3 text-n-amber-11'
|
||||
"
|
||||
>
|
||||
{{ account.status || '—' }}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
<BaseTableCell align="end" class="w-24">
|
||||
<div class="flex gap-2 justify-end flex-shrink-0">
|
||||
<Button
|
||||
v-tooltip.top="t('SUPER_ADMIN.COMMON.EDIT')"
|
||||
icon="i-woot-edit-pen"
|
||||
slate
|
||||
sm
|
||||
:is-loading="loading[account.id]"
|
||||
@click="openEditModal(account)"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.top="t('SUPER_ADMIN.COMMON.DELETE')"
|
||||
icon="i-woot-bin"
|
||||
slate
|
||||
sm
|
||||
class="hover:enabled:text-n-ruby-11 hover:enabled:bg-n-ruby-2"
|
||||
:is-loading="loading[account.id]"
|
||||
@click="openDeleteDialog(account)"
|
||||
/>
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
</template>
|
||||
</BaseTableRow>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-col items-center justify-center py-20 gap-2"
|
||||
>
|
||||
<span class="i-lucide-building-2 size-10 text-n-slate-10" />
|
||||
<p class="text-sm text-n-slate-10">
|
||||
{{ t('SUPER_ADMIN.ACCOUNTS.EMPTY') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormDialog
|
||||
ref="formDialogRef"
|
||||
:title="dialogTitle"
|
||||
:fields="formFields"
|
||||
:initial-data="selectedAccount"
|
||||
:is-loading="uiFlags.isCreating || uiFlags.isUpdating"
|
||||
@confirm="handleConfirm"
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
ref="deleteDialogRef"
|
||||
type="alert"
|
||||
:title="t('SUPER_ADMIN.ACCOUNTS.DELETE.TITLE')"
|
||||
:description="
|
||||
t('SUPER_ADMIN.ACCOUNTS.DELETE.MESSAGE', {
|
||||
name: selectedAccount?.name,
|
||||
})
|
||||
"
|
||||
:is-loading="uiFlags.isDeleting"
|
||||
:confirm-button-label="t('SUPER_ADMIN.COMMON.CONFIRM_DELETE')"
|
||||
:cancel-button-label="t('SUPER_ADMIN.COMMON.CANCEL')"
|
||||
@confirm="confirmDelete"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,252 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { picoSearch } from '@scmmishra/pico-search';
|
||||
|
||||
import PageHeader from '../components/PageHeader.vue';
|
||||
import FormDialog from '../components/FormDialog.vue';
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||
import {
|
||||
BaseTable,
|
||||
BaseTableRow,
|
||||
BaseTableCell,
|
||||
} from 'dashboard/components-next/table';
|
||||
|
||||
const MODAL_TYPES = { CREATE: 'create', EDIT: 'edit' };
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const agentBots = useMapGetter('platform/getPlatformAgentBots');
|
||||
const uiFlags = useMapGetter('platform/getUIFlags');
|
||||
|
||||
const searchQuery = ref('');
|
||||
const modalType = ref(MODAL_TYPES.CREATE);
|
||||
const selectedBot = ref({});
|
||||
const formDialogRef = ref(null);
|
||||
const deleteDialogRef = ref(null);
|
||||
const loading = ref({});
|
||||
|
||||
const isLoading = computed(() => uiFlags.value?.isFetchingAgentBots);
|
||||
|
||||
const filteredBots = computed(() => {
|
||||
const query = searchQuery.value.trim();
|
||||
if (!query) return agentBots.value;
|
||||
return picoSearch(agentBots.value, query, ['name', 'description']);
|
||||
});
|
||||
|
||||
const dialogTitle = computed(() =>
|
||||
modalType.value === MODAL_TYPES.CREATE
|
||||
? t('SUPER_ADMIN.AGENT_BOTS.CREATE.TITLE')
|
||||
: t('SUPER_ADMIN.AGENT_BOTS.EDIT.TITLE')
|
||||
);
|
||||
|
||||
const formFields = computed(() => [
|
||||
{
|
||||
key: 'name',
|
||||
label: t('SUPER_ADMIN.AGENT_BOTS.FIELDS.NAME'),
|
||||
placeholder: 'My Bot',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
label: t('SUPER_ADMIN.AGENT_BOTS.FIELDS.DESCRIPTION'),
|
||||
type: 'textarea',
|
||||
placeholder: t('SUPER_ADMIN.AGENT_BOTS.FIELDS.DESCRIPTION_PLACEHOLDER'),
|
||||
},
|
||||
{
|
||||
key: 'outgoing_url',
|
||||
label: t('SUPER_ADMIN.AGENT_BOTS.FIELDS.URL'),
|
||||
placeholder: 'https://example.com/webhook',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'bot_type',
|
||||
label: t('SUPER_ADMIN.AGENT_BOTS.FIELDS.TYPE'),
|
||||
placeholder: 'webhook',
|
||||
hint: t('SUPER_ADMIN.AGENT_BOTS.FIELDS.TYPE_HINT'),
|
||||
},
|
||||
]);
|
||||
|
||||
const openCreateModal = () => {
|
||||
modalType.value = MODAL_TYPES.CREATE;
|
||||
selectedBot.value = {};
|
||||
formDialogRef.value?.open();
|
||||
};
|
||||
|
||||
const openEditModal = bot => {
|
||||
modalType.value = MODAL_TYPES.EDIT;
|
||||
selectedBot.value = { ...bot };
|
||||
formDialogRef.value?.open();
|
||||
};
|
||||
|
||||
const openDeleteDialog = bot => {
|
||||
selectedBot.value = bot;
|
||||
deleteDialogRef.value?.open();
|
||||
};
|
||||
|
||||
const handleConfirm = async data => {
|
||||
try {
|
||||
if (modalType.value === MODAL_TYPES.CREATE) {
|
||||
await store.dispatch('platform/createAgentBot', data);
|
||||
useAlert(t('SUPER_ADMIN.AGENT_BOTS.CREATE.SUCCESS'));
|
||||
} else {
|
||||
await store.dispatch('platform/updateAgentBot', {
|
||||
id: selectedBot.value.id,
|
||||
data,
|
||||
});
|
||||
useAlert(t('SUPER_ADMIN.AGENT_BOTS.EDIT.SUCCESS'));
|
||||
}
|
||||
formDialogRef.value?.close();
|
||||
} catch (error) {
|
||||
useAlert(t('SUPER_ADMIN.AGENT_BOTS.ERROR'));
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDelete = async () => {
|
||||
loading.value[selectedBot.value.id] = true;
|
||||
try {
|
||||
await store.dispatch('platform/deleteAgentBot', selectedBot.value.id);
|
||||
useAlert(t('SUPER_ADMIN.AGENT_BOTS.DELETE.SUCCESS'));
|
||||
} catch (error) {
|
||||
useAlert(t('SUPER_ADMIN.AGENT_BOTS.ERROR'));
|
||||
} finally {
|
||||
loading.value[selectedBot.value.id] = false;
|
||||
deleteDialogRef.value?.close();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('platform/fetchAgentBots');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageHeader
|
||||
:title="t('SUPER_ADMIN.AGENT_BOTS.TITLE')"
|
||||
:description="t('SUPER_ADMIN.AGENT_BOTS.DESCRIPTION')"
|
||||
v-model:search-query="searchQuery"
|
||||
:search-placeholder="t('SUPER_ADMIN.AGENT_BOTS.SEARCH')"
|
||||
:button-label="t('SUPER_ADMIN.AGENT_BOTS.CREATE.TITLE')"
|
||||
:count="agentBots?.length"
|
||||
@click="openCreateModal"
|
||||
/>
|
||||
<div class="flex-1 px-6">
|
||||
<div class="w-full max-w-5xl mx-auto py-6">
|
||||
<div v-if="isLoading" class="flex items-center justify-center py-20">
|
||||
<Spinner />
|
||||
</div>
|
||||
<div
|
||||
v-else-if="filteredBots?.length"
|
||||
class="flex flex-col rounded-xl bg-n-background border border-n-weak overflow-hidden"
|
||||
>
|
||||
<BaseTable
|
||||
:headers="[
|
||||
t('SUPER_ADMIN.AGENT_BOTS.TABLE.NAME'),
|
||||
t('SUPER_ADMIN.AGENT_BOTS.TABLE.URL'),
|
||||
t('SUPER_ADMIN.AGENT_BOTS.TABLE.ACTIONS'),
|
||||
]"
|
||||
:items="filteredBots"
|
||||
>
|
||||
<template #row="{ items }">
|
||||
<BaseTableRow v-for="bot in items" :key="bot.id" :item="bot">
|
||||
<template #default>
|
||||
<BaseTableCell>
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<Avatar
|
||||
:name="bot.name"
|
||||
:src="bot.thumbnail"
|
||||
:size="36"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-n-slate-12 truncate">
|
||||
{{ bot.name }}
|
||||
</span>
|
||||
<span
|
||||
v-if="bot.system_bot"
|
||||
class="text-xs text-n-slate-12 bg-n-blue-5 rounded-md py-0.5 px-1 flex-shrink-0"
|
||||
>
|
||||
{{ t('SUPER_ADMIN.AGENT_BOTS.GLOBAL_BADGE') }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-xs text-n-slate-10 truncate block">
|
||||
{{ bot.description }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
<BaseTableCell>
|
||||
<span class="text-sm text-n-slate-11 truncate block">
|
||||
{{ bot.outgoing_url || bot.bot_config?.webhook_url || '—' }}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
<BaseTableCell align="end" class="w-24">
|
||||
<div class="flex gap-2 justify-end flex-shrink-0">
|
||||
<Button
|
||||
v-if="!bot.system_bot"
|
||||
v-tooltip.top="t('SUPER_ADMIN.COMMON.EDIT')"
|
||||
icon="i-woot-edit-pen"
|
||||
slate
|
||||
sm
|
||||
:is-loading="loading[bot.id]"
|
||||
@click="openEditModal(bot)"
|
||||
/>
|
||||
<Button
|
||||
v-if="!bot.system_bot"
|
||||
v-tooltip.top="t('SUPER_ADMIN.COMMON.DELETE')"
|
||||
icon="i-woot-bin"
|
||||
slate
|
||||
sm
|
||||
class="hover:enabled:text-n-ruby-11 hover:enabled:bg-n-ruby-2"
|
||||
:is-loading="loading[bot.id]"
|
||||
@click="openDeleteDialog(bot)"
|
||||
/>
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
</template>
|
||||
</BaseTableRow>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-col items-center justify-center py-20 gap-2"
|
||||
>
|
||||
<span class="i-lucide-bot size-10 text-n-slate-10" />
|
||||
<p class="text-sm text-n-slate-10">
|
||||
{{ t('SUPER_ADMIN.AGENT_BOTS.EMPTY') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormDialog
|
||||
ref="formDialogRef"
|
||||
:title="dialogTitle"
|
||||
:fields="formFields"
|
||||
:initial-data="selectedBot"
|
||||
:is-loading="uiFlags.isCreating || uiFlags.isUpdating"
|
||||
@confirm="handleConfirm"
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
ref="deleteDialogRef"
|
||||
type="alert"
|
||||
:title="t('SUPER_ADMIN.AGENT_BOTS.DELETE.TITLE')"
|
||||
:description="
|
||||
t('SUPER_ADMIN.AGENT_BOTS.DELETE.MESSAGE', { name: selectedBot?.name })
|
||||
"
|
||||
:is-loading="uiFlags.isDeleting"
|
||||
:confirm-button-label="t('SUPER_ADMIN.COMMON.CONFIRM_DELETE')"
|
||||
:cancel-button-label="t('SUPER_ADMIN.COMMON.CANCEL')"
|
||||
@confirm="confirmDelete"
|
||||
/>
|
||||
</template>
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import Textarea from 'dashboard/components-next/textarea/TextArea.vue';
|
||||
import Select from 'dashboard/components-next/select/Select.vue';
|
||||
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
fields: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
initialData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
confirmLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
cancelLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['confirm', 'cancel']);
|
||||
|
||||
const dialogRef = ref(null);
|
||||
const formData = ref({});
|
||||
|
||||
watch(
|
||||
() => props.initialData,
|
||||
(val) => {
|
||||
formData.value = { ...val };
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
const open = () => {
|
||||
formData.value = { ...props.initialData };
|
||||
dialogRef.value?.open();
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
dialogRef.value?.close();
|
||||
};
|
||||
|
||||
defineExpose({ open, close });
|
||||
|
||||
const handleConfirm = () => {
|
||||
emit('confirm', { ...formData.value });
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('cancel');
|
||||
close();
|
||||
};
|
||||
|
||||
const isFieldType = field => field.type === 'select';
|
||||
const isTextarea = field => field.type === 'textarea';
|
||||
const isCheckbox = field => field.type === 'checkbox';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
ref="dialogRef"
|
||||
:title="title"
|
||||
:is-loading="isLoading"
|
||||
:confirm-button-label="confirmLabel"
|
||||
:cancel-button-label="cancelLabel"
|
||||
@confirm="handleConfirm"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<div class="flex flex-col gap-4 py-2">
|
||||
<div
|
||||
v-for="field in fields"
|
||||
:key="field.key"
|
||||
class="flex flex-col gap-1.5"
|
||||
>
|
||||
<label
|
||||
v-if="field.label && !isCheckbox(field)"
|
||||
class="text-sm font-medium text-n-slate-12"
|
||||
>
|
||||
{{ field.label }}
|
||||
<span v-if="field.required" class="text-n-ruby-11">*</span>
|
||||
</label>
|
||||
|
||||
<!-- Select -->
|
||||
<Select
|
||||
v-if="isFieldType(field)"
|
||||
v-model="formData[field.key]"
|
||||
:options="field.options || []"
|
||||
:placeholder="field.placeholder || ''"
|
||||
class="w-full"
|
||||
/>
|
||||
|
||||
<!-- Textarea -->
|
||||
<Textarea
|
||||
v-else-if="isTextarea(field)"
|
||||
v-model="formData[field.key]"
|
||||
:placeholder="field.placeholder || ''"
|
||||
:rows="field.rows || 3"
|
||||
size="sm"
|
||||
/>
|
||||
|
||||
<!-- Checkbox -->
|
||||
<div
|
||||
v-else-if="isCheckbox(field)"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<Checkbox v-model="formData[field.key]" />
|
||||
<label class="text-sm text-n-slate-12 cursor-pointer">
|
||||
{{ field.label }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Input (default) -->
|
||||
<Input
|
||||
v-else
|
||||
v-model="formData[field.key]"
|
||||
:type="field.inputType || 'text'"
|
||||
:placeholder="field.placeholder || ''"
|
||||
size="sm"
|
||||
/>
|
||||
|
||||
<p v-if="field.hint" class="text-xs text-n-slate-10">
|
||||
{{ field.hint }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
<script setup>
|
||||
import { useSlots } from 'vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
searchPlaceholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
buttonLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
count: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['click', 'update:searchQuery']);
|
||||
const slots = useSlots();
|
||||
|
||||
const searchQuery = defineModel('searchQuery', { type: String, default: '' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="sticky top-0 z-10 px-6 bg-n-surface-1">
|
||||
<div class="w-full max-w-5xl mx-auto">
|
||||
<div
|
||||
class="flex items-start justify-between w-full py-6 gap-4"
|
||||
>
|
||||
<div class="flex flex-col gap-1.5 min-w-0">
|
||||
<h1 class="text-xl font-medium text-n-slate-12 truncate">
|
||||
{{ title }}
|
||||
</h1>
|
||||
<p v-if="description" class="text-sm text-n-slate-11 max-w-2xl">
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 flex-shrink-0">
|
||||
<Input
|
||||
v-if="searchPlaceholder"
|
||||
v-model="searchQuery"
|
||||
:placeholder="searchPlaceholder"
|
||||
class="group w-56 min-w-0 [&>input]:ltr:!pl-8 [&>input]:rtl:!pr-8 [&>input]:!rounded-[0.625rem]"
|
||||
size="sm"
|
||||
type="search"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon
|
||||
icon="i-lucide-search"
|
||||
class="absolute top-1/2 -translate-y-1/2 text-n-slate-11 group-focus-within:text-n-brand size-3.5 ltr:left-2.5 rtl:right-2.5"
|
||||
/>
|
||||
</template>
|
||||
</Input>
|
||||
<span
|
||||
v-if="count !== null"
|
||||
class="text-sm text-n-slate-11 flex-shrink-0"
|
||||
>
|
||||
{{ count }}
|
||||
</span>
|
||||
<Button
|
||||
v-if="buttonLabel"
|
||||
:label="buttonLabel"
|
||||
icon="i-lucide-plus"
|
||||
size="sm"
|
||||
@click="emit('click')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
@@ -0,0 +1,240 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { picoSearch } from '@scmmishra/pico-search';
|
||||
|
||||
import PageHeader from '../components/PageHeader.vue';
|
||||
import FormDialog from '../components/FormDialog.vue';
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import {
|
||||
BaseTable,
|
||||
BaseTableRow,
|
||||
BaseTableCell,
|
||||
} from 'dashboard/components-next/table';
|
||||
|
||||
const MODAL_TYPES = { CREATE: 'create', EDIT: 'edit' };
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const configs = useMapGetter('platform/getInstallationConfigs');
|
||||
const uiFlags = useMapGetter('platform/getUIFlags');
|
||||
|
||||
const searchQuery = ref('');
|
||||
const modalType = ref(MODAL_TYPES.CREATE);
|
||||
const selectedConfig = ref({});
|
||||
const formDialogRef = ref(null);
|
||||
const deleteDialogRef = ref(null);
|
||||
const loading = ref({});
|
||||
|
||||
const isLoading = computed(() => uiFlags.value?.isFetchingConfigs);
|
||||
|
||||
const filteredConfigs = computed(() => {
|
||||
const query = searchQuery.value.trim();
|
||||
if (!query) return configs.value;
|
||||
return picoSearch(configs.value, query, ['name', 'value']);
|
||||
});
|
||||
|
||||
const dialogTitle = computed(() =>
|
||||
modalType.value === MODAL_TYPES.CREATE
|
||||
? t('SUPER_ADMIN.CONFIGS.CREATE.TITLE')
|
||||
: t('SUPER_ADMIN.CONFIGS.EDIT.TITLE')
|
||||
);
|
||||
|
||||
const formFields = computed(() => [
|
||||
{
|
||||
key: 'name',
|
||||
label: t('SUPER_ADMIN.CONFIGS.FIELDS.NAME'),
|
||||
placeholder: 'INSTALLATION_NAME',
|
||||
required: true,
|
||||
hint: t('SUPER_ADMIN.CONFIGS.FIELDS.NAME_HINT'),
|
||||
},
|
||||
{
|
||||
key: 'value',
|
||||
label: t('SUPER_ADMIN.CONFIGS.FIELDS.VALUE'),
|
||||
type: 'textarea',
|
||||
placeholder: t('SUPER_ADMIN.CONFIGS.FIELDS.VALUE_PLACEHOLDER'),
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'locked',
|
||||
label: t('SUPER_ADMIN.CONFIGS.FIELDS.LOCKED'),
|
||||
type: 'checkbox',
|
||||
hint: t('SUPER_ADMIN.CONFIGS.FIELDS.LOCKED_HINT'),
|
||||
},
|
||||
]);
|
||||
|
||||
const openCreateModal = () => {
|
||||
modalType.value = MODAL_TYPES.CREATE;
|
||||
selectedConfig.value = {};
|
||||
formDialogRef.value?.open();
|
||||
};
|
||||
|
||||
const openEditModal = config => {
|
||||
modalType.value = MODAL_TYPES.EDIT;
|
||||
selectedConfig.value = { ...config };
|
||||
formDialogRef.value?.open();
|
||||
};
|
||||
|
||||
const openDeleteDialog = config => {
|
||||
selectedConfig.value = config;
|
||||
deleteDialogRef.value?.open();
|
||||
};
|
||||
|
||||
const handleConfirm = async data => {
|
||||
try {
|
||||
if (modalType.value === MODAL_TYPES.CREATE) {
|
||||
await store.dispatch('platform/createInstallationConfig', data);
|
||||
useAlert(t('SUPER_ADMIN.CONFIGS.CREATE.SUCCESS'));
|
||||
} else {
|
||||
await store.dispatch('platform/updateInstallationConfig', {
|
||||
id: selectedConfig.value.id,
|
||||
data,
|
||||
});
|
||||
useAlert(t('SUPER_ADMIN.CONFIGS.EDIT.SUCCESS'));
|
||||
}
|
||||
formDialogRef.value?.close();
|
||||
} catch (error) {
|
||||
useAlert(t('SUPER_ADMIN.CONFIGS.ERROR'));
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDelete = async () => {
|
||||
loading.value[selectedConfig.value.id] = true;
|
||||
try {
|
||||
await store.dispatch(
|
||||
'platform/deleteInstallationConfig',
|
||||
selectedConfig.value.id
|
||||
);
|
||||
useAlert(t('SUPER_ADMIN.CONFIGS.DELETE.SUCCESS'));
|
||||
} catch (error) {
|
||||
useAlert(t('SUPER_ADMIN.CONFIGS.ERROR'));
|
||||
} finally {
|
||||
loading.value[selectedConfig.value.id] = false;
|
||||
deleteDialogRef.value?.close();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('platform/fetchInstallationConfigs');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageHeader
|
||||
:title="t('SUPER_ADMIN.CONFIGS.TITLE')"
|
||||
:description="t('SUPER_ADMIN.CONFIGS.DESCRIPTION')"
|
||||
v-model:search-query="searchQuery"
|
||||
:search-placeholder="t('SUPER_ADMIN.CONFIGS.SEARCH')"
|
||||
:button-label="t('SUPER_ADMIN.CONFIGS.CREATE.TITLE')"
|
||||
:count="configs?.length"
|
||||
@click="openCreateModal"
|
||||
/>
|
||||
<div class="flex-1 px-6">
|
||||
<div class="w-full max-w-5xl mx-auto py-6">
|
||||
<div v-if="isLoading" class="flex items-center justify-center py-20">
|
||||
<Spinner />
|
||||
</div>
|
||||
<div
|
||||
v-else-if="filteredConfigs?.length"
|
||||
class="flex flex-col rounded-xl bg-n-background border border-n-weak overflow-hidden"
|
||||
>
|
||||
<BaseTable
|
||||
:headers="[
|
||||
t('SUPER_ADMIN.CONFIGS.TABLE.NAME'),
|
||||
t('SUPER_ADMIN.CONFIGS.TABLE.VALUE'),
|
||||
t('SUPER_ADMIN.CONFIGS.TABLE.ACTIONS'),
|
||||
]"
|
||||
:items="filteredConfigs"
|
||||
>
|
||||
<template #row="{ items }">
|
||||
<BaseTableRow
|
||||
v-for="config in items"
|
||||
:key="config.id"
|
||||
:item="config"
|
||||
>
|
||||
<template #default>
|
||||
<BaseTableCell class="max-w-xs">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<span
|
||||
class="text-sm font-medium text-n-slate-12 truncate"
|
||||
>
|
||||
{{ config.name }}
|
||||
</span>
|
||||
<span
|
||||
v-if="config.locked"
|
||||
class="i-lucide-lock text-n-amber-11 size-3.5 flex-shrink-0"
|
||||
/>
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
<BaseTableCell class="max-w-xs">
|
||||
<span class="text-sm text-n-slate-11 truncate block">
|
||||
{{ config.value || '—' }}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
<BaseTableCell align="end" class="w-24">
|
||||
<div class="flex gap-2 justify-end flex-shrink-0">
|
||||
<Button
|
||||
v-tooltip.top="t('SUPER_ADMIN.COMMON.EDIT')"
|
||||
icon="i-woot-edit-pen"
|
||||
slate
|
||||
sm
|
||||
:is-loading="loading[config.id]"
|
||||
@click="openEditModal(config)"
|
||||
/>
|
||||
<Button
|
||||
v-if="!config.locked"
|
||||
v-tooltip.top="t('SUPER_ADMIN.COMMON.DELETE')"
|
||||
icon="i-woot-bin"
|
||||
slate
|
||||
sm
|
||||
class="hover:enabled:text-n-ruby-11 hover:enabled:bg-n-ruby-2"
|
||||
:is-loading="loading[config.id]"
|
||||
@click="openDeleteDialog(config)"
|
||||
/>
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
</template>
|
||||
</BaseTableRow>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-col items-center justify-center py-20 gap-2"
|
||||
>
|
||||
<span class="i-lucide-settings-2 size-10 text-n-slate-10" />
|
||||
<p class="text-sm text-n-slate-10">
|
||||
{{ t('SUPER_ADMIN.CONFIGS.EMPTY') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormDialog
|
||||
ref="formDialogRef"
|
||||
:title="dialogTitle"
|
||||
:fields="formFields"
|
||||
:initial-data="selectedConfig"
|
||||
:is-loading="uiFlags.isCreating || uiFlags.isUpdating"
|
||||
@confirm="handleConfirm"
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
ref="deleteDialogRef"
|
||||
type="alert"
|
||||
:title="t('SUPER_ADMIN.CONFIGS.DELETE.TITLE')"
|
||||
:description="
|
||||
t('SUPER_ADMIN.CONFIGS.DELETE.MESSAGE', { name: selectedConfig?.name })
|
||||
"
|
||||
:is-loading="uiFlags.isDeleting"
|
||||
:confirm-button-label="t('SUPER_ADMIN.COMMON.CONFIRM_DELETE')"
|
||||
:cancel-button-label="t('SUPER_ADMIN.COMMON.CANCEL')"
|
||||
@confirm="confirmDelete"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,143 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
|
||||
import PageHeader from '../components/PageHeader.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
import {
|
||||
BaseTable,
|
||||
BaseTableRow,
|
||||
BaseTableCell,
|
||||
} from 'dashboard/components-next/table';
|
||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const accounts = useMapGetter('platform/getAccounts');
|
||||
const users = useMapGetter('platform/getUsers');
|
||||
const uiFlags = useMapGetter('platform/getUIFlags');
|
||||
|
||||
const stats = computed(() => [
|
||||
{
|
||||
label: t('SUPER_ADMIN.DASHBOARD.STATS.ACCOUNTS'),
|
||||
value: accounts.value?.length ?? 0,
|
||||
icon: 'i-lucide-building-2',
|
||||
color: 'text-n-blue-11 bg-n-blue-3',
|
||||
},
|
||||
{
|
||||
label: t('SUPER_ADMIN.DASHBOARD.STATS.USERS'),
|
||||
value: users.value?.length ?? 0,
|
||||
icon: 'i-lucide-users',
|
||||
color: 'text-n-green-11 bg-n-green-3',
|
||||
},
|
||||
]);
|
||||
|
||||
const isLoading = computed(
|
||||
() => uiFlags.value?.isFetchingAccounts || uiFlags.value?.isFetchingUsers
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('platform/fetchAccounts');
|
||||
store.dispatch('platform/fetchUsers');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageHeader
|
||||
:title="t('SUPER_ADMIN.DASHBOARD.TITLE')"
|
||||
:description="t('SUPER_ADMIN.DASHBOARD.DESCRIPTION')"
|
||||
/>
|
||||
<div class="flex-1 px-6">
|
||||
<div class="w-full max-w-5xl mx-auto py-6">
|
||||
<div v-if="isLoading" class="flex items-center justify-center py-20">
|
||||
<Spinner />
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-6">
|
||||
<!-- Stats Cards -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div
|
||||
v-for="stat in stats"
|
||||
:key="stat.label"
|
||||
class="flex items-center gap-4 p-5 rounded-xl bg-n-background border border-n-weak"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-center size-12 rounded-lg flex-shrink-0"
|
||||
:class="stat.color"
|
||||
>
|
||||
<span class="text-xl" :class="stat.icon" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="text-2xl font-semibold text-n-slate-12">
|
||||
{{ stat.value }}
|
||||
</div>
|
||||
<div class="text-sm text-n-slate-11 truncate">
|
||||
{{ stat.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Accounts -->
|
||||
<div
|
||||
class="flex flex-col rounded-xl bg-n-background border border-n-weak overflow-hidden"
|
||||
>
|
||||
<div class="px-5 py-4 border-b border-n-weak">
|
||||
<h2 class="text-base font-medium text-n-slate-12">
|
||||
{{ t('SUPER_ADMIN.DASHBOARD.RECENT_ACCOUNTS') }}
|
||||
</h2>
|
||||
</div>
|
||||
<BaseTable
|
||||
v-if="accounts?.length"
|
||||
:headers="[
|
||||
t('SUPER_ADMIN.ACCOUNTS.TABLE.NAME'),
|
||||
t('SUPER_ADMIN.ACCOUNTS.TABLE.STATUS'),
|
||||
]"
|
||||
:items="accounts.slice(0, 5)"
|
||||
>
|
||||
<template #row="{ items }">
|
||||
<BaseTableRow
|
||||
v-for="account in items"
|
||||
:key="account.id"
|
||||
:item="account"
|
||||
>
|
||||
<template #default>
|
||||
<BaseTableCell>
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<Avatar
|
||||
:name="account.name"
|
||||
:size="32"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
<span class="text-sm text-n-slate-12 truncate">
|
||||
{{ account.name }}
|
||||
</span>
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
<BaseTableCell>
|
||||
<span
|
||||
class="text-xs px-2 py-0.5 rounded-md"
|
||||
:class="
|
||||
account.status === 'active'
|
||||
? 'bg-n-green-3 text-n-green-11'
|
||||
: 'bg-n-amber-3 text-n-amber-11'
|
||||
"
|
||||
>
|
||||
{{ account.status || '—' }}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
</template>
|
||||
</BaseTableRow>
|
||||
</template>
|
||||
</BaseTable>
|
||||
<div v-else class="px-5 py-10 text-center text-sm text-n-slate-10">
|
||||
{{ t('SUPER_ADMIN.ACCOUNTS.EMPTY') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,53 @@
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
|
||||
import SuperAdminLayout from './Layout.vue';
|
||||
import DashboardIndex from './dashboard/Index.vue';
|
||||
import AccountsIndex from './accounts/Index.vue';
|
||||
import UsersIndex from './users/Index.vue';
|
||||
import AgentBotsIndex from './agentBots/Index.vue';
|
||||
import ConfigsIndex from './configs/Index.vue';
|
||||
|
||||
const meta = {
|
||||
permissions: ['administrator', 'agent', 'custom_role'],
|
||||
isSuperAdmin: true,
|
||||
};
|
||||
|
||||
export const routes = [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/super_admin'),
|
||||
component: SuperAdminLayout,
|
||||
meta,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'super_admin_dashboard',
|
||||
component: DashboardIndex,
|
||||
meta,
|
||||
},
|
||||
{
|
||||
path: 'accounts',
|
||||
name: 'super_admin_accounts',
|
||||
component: AccountsIndex,
|
||||
meta,
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
name: 'super_admin_users',
|
||||
component: UsersIndex,
|
||||
meta,
|
||||
},
|
||||
{
|
||||
path: 'agent_bots',
|
||||
name: 'super_admin_agent_bots',
|
||||
component: AgentBotsIndex,
|
||||
meta,
|
||||
},
|
||||
{
|
||||
path: 'configs',
|
||||
name: 'super_admin_configs',
|
||||
component: ConfigsIndex,
|
||||
meta,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,255 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { picoSearch } from '@scmmishra/pico-search';
|
||||
|
||||
import PageHeader from '../components/PageHeader.vue';
|
||||
import FormDialog from '../components/FormDialog.vue';
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||
import {
|
||||
BaseTable,
|
||||
BaseTableRow,
|
||||
BaseTableCell,
|
||||
} from 'dashboard/components-next/table';
|
||||
|
||||
const MODAL_TYPES = { CREATE: 'create', EDIT: 'edit' };
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const users = useMapGetter('platform/getUsers');
|
||||
const uiFlags = useMapGetter('platform/getUIFlags');
|
||||
|
||||
const searchQuery = ref('');
|
||||
const modalType = ref(MODAL_TYPES.CREATE);
|
||||
const selectedUser = ref({});
|
||||
const formDialogRef = ref(null);
|
||||
const deleteDialogRef = ref(null);
|
||||
const loading = ref({});
|
||||
|
||||
const isLoading = computed(() => uiFlags.value?.isFetchingUsers);
|
||||
|
||||
const filteredUsers = computed(() => {
|
||||
const query = searchQuery.value.trim();
|
||||
if (!query) return users.value;
|
||||
return picoSearch(users.value, query, ['name', 'email']);
|
||||
});
|
||||
|
||||
const dialogTitle = computed(() =>
|
||||
modalType.value === MODAL_TYPES.CREATE
|
||||
? t('SUPER_ADMIN.USERS.CREATE.TITLE')
|
||||
: t('SUPER_ADMIN.USERS.EDIT.TITLE')
|
||||
);
|
||||
|
||||
const formFields = computed(() => [
|
||||
{
|
||||
key: 'name',
|
||||
label: t('SUPER_ADMIN.USERS.FIELDS.NAME'),
|
||||
placeholder: t('SUPER_ADMIN.USERS.FIELDS.NAME_PLACEHOLDER'),
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'email',
|
||||
label: t('SUPER_ADMIN.USERS.FIELDS.EMAIL'),
|
||||
placeholder: 'user@example.com',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
label: t('SUPER_ADMIN.USERS.FIELDS.PASSWORD'),
|
||||
placeholder: '••••••••',
|
||||
hint: t('SUPER_ADMIN.USERS.FIELDS.PASSWORD_HINT'),
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
label: t('SUPER_ADMIN.USERS.FIELDS.TYPE'),
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: 'user', label: t('SUPER_ADMIN.USERS.FIELDS.TYPE_USER') },
|
||||
{ value: 'SuperAdmin', label: t('SUPER_ADMIN.USERS.FIELDS.TYPE_SUPER_ADMIN') },
|
||||
],
|
||||
hint: t('SUPER_ADMIN.USERS.FIELDS.TYPE_HINT'),
|
||||
},
|
||||
]);
|
||||
|
||||
const openCreateModal = () => {
|
||||
modalType.value = MODAL_TYPES.CREATE;
|
||||
selectedUser.value = { type: 'user' };
|
||||
formDialogRef.value?.open();
|
||||
};
|
||||
|
||||
const openEditModal = user => {
|
||||
modalType.value = MODAL_TYPES.EDIT;
|
||||
selectedUser.value = { ...user, password: '' };
|
||||
formDialogRef.value?.open();
|
||||
};
|
||||
|
||||
const openDeleteDialog = user => {
|
||||
selectedUser.value = user;
|
||||
deleteDialogRef.value?.open();
|
||||
};
|
||||
|
||||
const handleConfirm = async data => {
|
||||
try {
|
||||
if (modalType.value === MODAL_TYPES.CREATE) {
|
||||
await store.dispatch('platform/createUser', data);
|
||||
useAlert(t('SUPER_ADMIN.USERS.CREATE.SUCCESS'));
|
||||
} else {
|
||||
const updateData = { ...data };
|
||||
if (!updateData.password) delete updateData.password;
|
||||
await store.dispatch('platform/updateUser', {
|
||||
id: selectedUser.value.id,
|
||||
data: updateData,
|
||||
});
|
||||
useAlert(t('SUPER_ADMIN.USERS.EDIT.SUCCESS'));
|
||||
}
|
||||
formDialogRef.value?.close();
|
||||
} catch (error) {
|
||||
useAlert(t('SUPER_ADMIN.USERS.ERROR'));
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDelete = async () => {
|
||||
loading.value[selectedUser.value.id] = true;
|
||||
try {
|
||||
await store.dispatch('platform/deleteUser', selectedUser.value.id);
|
||||
useAlert(t('SUPER_ADMIN.USERS.DELETE.SUCCESS'));
|
||||
} catch (error) {
|
||||
useAlert(t('SUPER_ADMIN.USERS.ERROR'));
|
||||
} finally {
|
||||
loading.value[selectedUser.value.id] = false;
|
||||
deleteDialogRef.value?.close();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('platform/fetchUsers');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageHeader
|
||||
:title="t('SUPER_ADMIN.USERS.TITLE')"
|
||||
:description="t('SUPER_ADMIN.USERS.DESCRIPTION')"
|
||||
v-model:search-query="searchQuery"
|
||||
:search-placeholder="t('SUPER_ADMIN.USERS.SEARCH')"
|
||||
:button-label="t('SUPER_ADMIN.USERS.CREATE.TITLE')"
|
||||
:count="users?.length"
|
||||
@click="openCreateModal"
|
||||
/>
|
||||
<div class="flex-1 px-6">
|
||||
<div class="w-full max-w-5xl mx-auto py-6">
|
||||
<div v-if="isLoading" class="flex items-center justify-center py-20">
|
||||
<Spinner />
|
||||
</div>
|
||||
<div
|
||||
v-else-if="filteredUsers?.length"
|
||||
class="flex flex-col rounded-xl bg-n-background border border-n-weak overflow-hidden"
|
||||
>
|
||||
<BaseTable
|
||||
:headers="[
|
||||
t('SUPER_ADMIN.USERS.TABLE.NAME'),
|
||||
t('SUPER_ADMIN.USERS.TABLE.TYPE'),
|
||||
t('SUPER_ADMIN.USERS.TABLE.ACTIONS'),
|
||||
]"
|
||||
:items="filteredUsers"
|
||||
>
|
||||
<template #row="{ items }">
|
||||
<BaseTableRow v-for="user in items" :key="user.id" :item="user">
|
||||
<template #default>
|
||||
<BaseTableCell>
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<Avatar
|
||||
:name="user.name || user.available_name"
|
||||
:src="user.avatar_url"
|
||||
:size="36"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
<div class="min-w-0">
|
||||
<span class="text-sm text-n-slate-12 truncate block">
|
||||
{{ user.name || user.available_name }}
|
||||
</span>
|
||||
<span class="text-xs text-n-slate-10 truncate block">
|
||||
{{ user.email }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
<BaseTableCell>
|
||||
<span
|
||||
class="text-xs px-2 py-0.5 rounded-md"
|
||||
:class="
|
||||
user.type === 'SuperAdmin'
|
||||
? 'bg-n-violet-3 text-n-violet-11'
|
||||
: 'bg-n-slate-3 text-n-slate-11'
|
||||
"
|
||||
>
|
||||
{{ user.type || 'User' }}
|
||||
</span>
|
||||
</BaseTableCell>
|
||||
<BaseTableCell align="end" class="w-24">
|
||||
<div class="flex gap-2 justify-end flex-shrink-0">
|
||||
<Button
|
||||
v-tooltip.top="t('SUPER_ADMIN.COMMON.EDIT')"
|
||||
icon="i-woot-edit-pen"
|
||||
slate
|
||||
sm
|
||||
:is-loading="loading[user.id]"
|
||||
@click="openEditModal(user)"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.top="t('SUPER_ADMIN.COMMON.DELETE')"
|
||||
icon="i-woot-bin"
|
||||
slate
|
||||
sm
|
||||
class="hover:enabled:text-n-ruby-11 hover:enabled:bg-n-ruby-2"
|
||||
:is-loading="loading[user.id]"
|
||||
@click="openDeleteDialog(user)"
|
||||
/>
|
||||
</div>
|
||||
</BaseTableCell>
|
||||
</template>
|
||||
</BaseTableRow>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-col items-center justify-center py-20 gap-2"
|
||||
>
|
||||
<span class="i-lucide-users size-10 text-n-slate-10" />
|
||||
<p class="text-sm text-n-slate-10">
|
||||
{{ t('SUPER_ADMIN.USERS.EMPTY') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormDialog
|
||||
ref="formDialogRef"
|
||||
:title="dialogTitle"
|
||||
:fields="formFields"
|
||||
:initial-data="selectedUser"
|
||||
:is-loading="uiFlags.isCreating || uiFlags.isUpdating"
|
||||
@confirm="handleConfirm"
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
ref="deleteDialogRef"
|
||||
type="alert"
|
||||
:title="t('SUPER_ADMIN.USERS.DELETE.TITLE')"
|
||||
:description="
|
||||
t('SUPER_ADMIN.USERS.DELETE.MESSAGE', { name: selectedUser?.name })
|
||||
"
|
||||
:is-loading="uiFlags.isDeleting"
|
||||
:confirm-button-label="t('SUPER_ADMIN.COMMON.CONFIRM_DELETE')"
|
||||
:cancel-button-label="t('SUPER_ADMIN.COMMON.CANCEL')"
|
||||
@confirm="confirmDelete"
|
||||
/>
|
||||
</template>
|
||||
@@ -59,6 +59,7 @@ import copilotMessages from './captain/copilotMessages';
|
||||
import captainScenarios from './captain/scenarios';
|
||||
import captainTools from './captain/tools';
|
||||
import captainCustomTools from './captain/customTools';
|
||||
import platform from './modules/platform';
|
||||
|
||||
const plugins = [];
|
||||
|
||||
@@ -123,6 +124,7 @@ export default createStore({
|
||||
captainScenarios,
|
||||
captainTools,
|
||||
captainCustomTools,
|
||||
platform,
|
||||
},
|
||||
plugins,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
import types from '../mutation-types';
|
||||
import PlatformAPI from '../../api/platform';
|
||||
import { throwErrorMessage } from '../utils/api';
|
||||
|
||||
export const state = {
|
||||
accounts: [],
|
||||
users: [],
|
||||
agentBots: [],
|
||||
installationConfigs: [],
|
||||
uiFlags: {
|
||||
isFetchingAccounts: false,
|
||||
isFetchingUsers: false,
|
||||
isFetchingAgentBots: false,
|
||||
isFetchingConfigs: false,
|
||||
isCreating: false,
|
||||
isUpdating: false,
|
||||
isDeleting: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const getters = {
|
||||
getAccounts: $state => $state.accounts,
|
||||
getUsers: $state => $state.users,
|
||||
getPlatformAgentBots: $state => $state.agentBots,
|
||||
getInstallationConfigs: $state => $state.installationConfigs,
|
||||
getUIFlags: $state => $state.uiFlags,
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
// ── Accounts ──────────────────────────────────────────
|
||||
fetchAccounts: async ({ commit }, params) => {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isFetchingAccounts: true });
|
||||
try {
|
||||
const response = await PlatformAPI.getAccounts(params);
|
||||
const payload = response.data?.data || response.data || [];
|
||||
commit(types.SET_PLATFORM_ACCOUNTS, payload);
|
||||
return payload;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isFetchingAccounts: false });
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
createAccount: async ({ commit }, accountData) => {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isCreating: true });
|
||||
try {
|
||||
const response = await PlatformAPI.createAccount(accountData);
|
||||
const payload = response.data?.data || response.data;
|
||||
commit(types.ADD_PLATFORM_ACCOUNT, payload);
|
||||
return payload;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isCreating: false });
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
updateAccount: async ({ commit }, { id, data }) => {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isUpdating: true });
|
||||
try {
|
||||
const response = await PlatformAPI.updateAccount(id, data);
|
||||
commit(types.EDIT_PLATFORM_ACCOUNT, response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isUpdating: false });
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
deleteAccount: async ({ commit }, id) => {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isDeleting: true });
|
||||
try {
|
||||
await PlatformAPI.deleteAccount(id);
|
||||
commit(types.DELETE_PLATFORM_ACCOUNT, id);
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isDeleting: false });
|
||||
}
|
||||
},
|
||||
|
||||
// ── Users ─────────────────────────────────────────────
|
||||
fetchUsers: async ({ commit }, params) => {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isFetchingUsers: true });
|
||||
try {
|
||||
const response = await PlatformAPI.getUsers(params);
|
||||
const payload = response.data?.data || response.data || [];
|
||||
commit(types.SET_PLATFORM_USERS, payload);
|
||||
return payload;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isFetchingUsers: false });
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
createUser: async ({ commit }, userData) => {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isCreating: true });
|
||||
try {
|
||||
const response = await PlatformAPI.createUser(userData);
|
||||
const payload = response.data?.data || response.data;
|
||||
commit(types.ADD_PLATFORM_USER, payload);
|
||||
return payload;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isCreating: false });
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
updateUser: async ({ commit }, { id, data }) => {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isUpdating: true });
|
||||
try {
|
||||
const response = await PlatformAPI.updateUser(id, data);
|
||||
commit(types.EDIT_PLATFORM_USER, response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isUpdating: false });
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
deleteUser: async ({ commit }, id) => {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isDeleting: true });
|
||||
try {
|
||||
await PlatformAPI.deleteUser(id);
|
||||
commit(types.DELETE_PLATFORM_USER, id);
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isDeleting: false });
|
||||
}
|
||||
},
|
||||
|
||||
// ── Agent Bots (platform-level) ───────────────────────
|
||||
fetchAgentBots: async ({ commit }, params) => {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isFetchingAgentBots: true });
|
||||
try {
|
||||
const response = await PlatformAPI.getAgentBots(params);
|
||||
const payload = response.data?.data || response.data || [];
|
||||
commit(types.SET_PLATFORM_AGENT_BOTS, payload);
|
||||
return payload;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isFetchingAgentBots: false });
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
createAgentBot: async ({ commit }, botData) => {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isCreating: true });
|
||||
try {
|
||||
const response = await PlatformAPI.createAgentBot(botData);
|
||||
const payload = response.data?.data || response.data;
|
||||
commit(types.ADD_PLATFORM_AGENT_BOT, payload);
|
||||
return payload;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isCreating: false });
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
updateAgentBot: async ({ commit }, { id, data }) => {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isUpdating: true });
|
||||
try {
|
||||
const response = await PlatformAPI.updateAgentBot(id, data);
|
||||
commit(types.EDIT_PLATFORM_AGENT_BOT, response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isUpdating: false });
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
deleteAgentBot: async ({ commit }, id) => {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isDeleting: true });
|
||||
try {
|
||||
await PlatformAPI.deleteAgentBot(id);
|
||||
commit(types.DELETE_PLATFORM_AGENT_BOT, id);
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isDeleting: false });
|
||||
}
|
||||
},
|
||||
|
||||
// ── Installation Config ───────────────────────────────
|
||||
fetchInstallationConfigs: async ({ commit }, params) => {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isFetchingConfigs: true });
|
||||
try {
|
||||
const response = await PlatformAPI.getInstallationConfigs(params);
|
||||
const payload = response.data?.data || response.data || [];
|
||||
commit(types.SET_INSTALLATION_CONFIGS, payload);
|
||||
return payload;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isFetchingConfigs: false });
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
createInstallationConfig: async ({ commit }, configData) => {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isCreating: true });
|
||||
try {
|
||||
const response = await PlatformAPI.createInstallationConfig(configData);
|
||||
const payload = response.data?.data || response.data;
|
||||
commit(types.ADD_INSTALLATION_CONFIG, payload);
|
||||
return payload;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isCreating: false });
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
updateInstallationConfig: async ({ commit }, { id, data }) => {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isUpdating: true });
|
||||
try {
|
||||
const response = await PlatformAPI.updateInstallationConfig(id, data);
|
||||
commit(types.EDIT_INSTALLATION_CONFIG, response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isUpdating: false });
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
deleteInstallationConfig: async ({ commit }, id) => {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isDeleting: true });
|
||||
try {
|
||||
await PlatformAPI.deleteInstallationConfig(id);
|
||||
commit(types.DELETE_INSTALLATION_CONFIG, id);
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isDeleting: false });
|
||||
}
|
||||
},
|
||||
|
||||
// ── Banners ───────────────────────────────────────────
|
||||
fetchBanners: async ({ commit }, params) => {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isFetchingBanners: true });
|
||||
try {
|
||||
const response = await PlatformAPI.getBanners(params);
|
||||
const payload = response.data?.data || response.data || [];
|
||||
commit(types.SET_PLATFORM_BANNERS, payload);
|
||||
return payload;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isFetchingBanners: false });
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
createBanner: async ({ commit }, bannerData) => {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isCreating: true });
|
||||
try {
|
||||
const response = await PlatformAPI.createBanner(bannerData);
|
||||
const payload = response.data?.data || response.data;
|
||||
commit(types.ADD_PLATFORM_BANNER, payload);
|
||||
return payload;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isCreating: false });
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
updateBanner: async ({ commit }, { id, data }) => {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isUpdating: true });
|
||||
try {
|
||||
const response = await PlatformAPI.updateBanner(id, data);
|
||||
commit(types.EDIT_PLATFORM_BANNER, response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isUpdating: false });
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
deleteBanner: async ({ commit }, id) => {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isDeleting: true });
|
||||
try {
|
||||
await PlatformAPI.deleteBanner(id);
|
||||
commit(types.DELETE_PLATFORM_BANNER, id);
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.SET_PLATFORM_UI_FLAG, { isDeleting: false });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
[types.SET_PLATFORM_UI_FLAG]($state, data) {
|
||||
$state.uiFlags = { ...$state.uiFlags, ...data };
|
||||
},
|
||||
|
||||
// Accounts
|
||||
[types.SET_PLATFORM_ACCOUNTS]($state, data) {
|
||||
$state.accounts = Array.isArray(data) ? data : data?.payload || [];
|
||||
},
|
||||
[types.ADD_PLATFORM_ACCOUNT]($state, account) {
|
||||
$state.accounts.unshift(account);
|
||||
},
|
||||
[types.EDIT_PLATFORM_ACCOUNT]($state, account) {
|
||||
const idx = $state.accounts.findIndex(a => a.id === account.id);
|
||||
if (idx !== -1) $state.accounts[idx] = account;
|
||||
},
|
||||
[types.DELETE_PLATFORM_ACCOUNT]($state, id) {
|
||||
$state.accounts = $state.accounts.filter(a => a.id !== id);
|
||||
},
|
||||
|
||||
// Users
|
||||
[types.SET_PLATFORM_USERS]($state, data) {
|
||||
$state.users = Array.isArray(data) ? data : data?.payload || [];
|
||||
},
|
||||
[types.ADD_PLATFORM_USER]($state, user) {
|
||||
$state.users.unshift(user);
|
||||
},
|
||||
[types.EDIT_PLATFORM_USER]($state, user) {
|
||||
const idx = $state.users.findIndex(u => u.id === user.id);
|
||||
if (idx !== -1) $state.users[idx] = user;
|
||||
},
|
||||
[types.DELETE_PLATFORM_USER]($state, id) {
|
||||
$state.users = $state.users.filter(u => u.id !== id);
|
||||
},
|
||||
|
||||
// Agent Bots
|
||||
[types.SET_PLATFORM_AGENT_BOTS]($state, data) {
|
||||
$state.agentBots = Array.isArray(data) ? data : data?.payload || [];
|
||||
},
|
||||
[types.ADD_PLATFORM_AGENT_BOT]($state, bot) {
|
||||
$state.agentBots.unshift(bot);
|
||||
},
|
||||
[types.EDIT_PLATFORM_AGENT_BOT]($state, bot) {
|
||||
const idx = $state.agentBots.findIndex(b => b.id === bot.id);
|
||||
if (idx !== -1) $state.agentBots[idx] = bot;
|
||||
},
|
||||
[types.DELETE_PLATFORM_AGENT_BOT]($state, id) {
|
||||
$state.agentBots = $state.agentBots.filter(b => b.id !== id);
|
||||
},
|
||||
|
||||
// Installation Configs
|
||||
[types.SET_INSTALLATION_CONFIGS]($state, data) {
|
||||
$state.installationConfigs = Array.isArray(data)
|
||||
? data
|
||||
: data?.payload || [];
|
||||
},
|
||||
[types.ADD_INSTALLATION_CONFIG]($state, config) {
|
||||
$state.installationConfigs.unshift(config);
|
||||
},
|
||||
[types.EDIT_INSTALLATION_CONFIG]($state, config) {
|
||||
const idx = $state.installationConfigs.findIndex(
|
||||
c => c.id === config.id
|
||||
);
|
||||
if (idx !== -1) $state.installationConfigs[idx] = config;
|
||||
},
|
||||
[types.DELETE_INSTALLATION_CONFIG]($state, id) {
|
||||
$state.installationConfigs = $state.installationConfigs.filter(
|
||||
c => c.id !== id
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
actions,
|
||||
state,
|
||||
getters,
|
||||
mutations,
|
||||
};
|
||||
@@ -392,4 +392,23 @@ export default {
|
||||
EDIT_AGENT_CAPACITY_POLICIES_INBOXES: 'EDIT_AGENT_CAPACITY_POLICIES_INBOXES',
|
||||
DELETE_AGENT_CAPACITY_POLICIES_INBOXES:
|
||||
'DELETE_AGENT_CAPACITY_POLICIES_INBOXES',
|
||||
|
||||
// Platform (Super Admin)
|
||||
SET_PLATFORM_UI_FLAG: 'SET_PLATFORM_UI_FLAG',
|
||||
SET_PLATFORM_ACCOUNTS: 'SET_PLATFORM_ACCOUNTS',
|
||||
ADD_PLATFORM_ACCOUNT: 'ADD_PLATFORM_ACCOUNT',
|
||||
EDIT_PLATFORM_ACCOUNT: 'EDIT_PLATFORM_ACCOUNT',
|
||||
DELETE_PLATFORM_ACCOUNT: 'DELETE_PLATFORM_ACCOUNT',
|
||||
SET_PLATFORM_USERS: 'SET_PLATFORM_USERS',
|
||||
ADD_PLATFORM_USER: 'ADD_PLATFORM_USER',
|
||||
EDIT_PLATFORM_USER: 'EDIT_PLATFORM_USER',
|
||||
DELETE_PLATFORM_USER: 'DELETE_PLATFORM_USER',
|
||||
SET_PLATFORM_AGENT_BOTS: 'SET_PLATFORM_AGENT_BOTS',
|
||||
ADD_PLATFORM_AGENT_BOT: 'ADD_PLATFORM_AGENT_BOT',
|
||||
EDIT_PLATFORM_AGENT_BOT: 'EDIT_PLATFORM_AGENT_BOT',
|
||||
DELETE_PLATFORM_AGENT_BOT: 'DELETE_PLATFORM_AGENT_BOT',
|
||||
SET_INSTALLATION_CONFIGS: 'SET_INSTALLATION_CONFIGS',
|
||||
ADD_INSTALLATION_CONFIG: 'ADD_INSTALLATION_CONFIG',
|
||||
EDIT_INSTALLATION_CONFIG: 'EDIT_INSTALLATION_CONFIG',
|
||||
DELETE_INSTALLATION_CONFIG: 'DELETE_INSTALLATION_CONFIG',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user