|
|
@@ -112,29 +112,124 @@
|
|
|
|
|
|
<!-- Dashboard Tab -->
|
|
|
<div x-show="tab === 'dashboard'" class="space-y-6">
|
|
|
- <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
|
- <!-- Stat Card -->
|
|
|
- <div class="bg-dark-800 p-6 rounded-2xl border border-dark-700">
|
|
|
- <div class="text-slate-400 mb-2">Total Users</div>
|
|
|
- <div class="text-3xl font-bold text-white" x-text="users.length || '-'"></div>
|
|
|
- </div>
|
|
|
- <div class="bg-dark-800 p-6 rounded-2xl border border-dark-700">
|
|
|
- <div class="text-slate-400 mb-2">Cluster Status</div>
|
|
|
- <div class="text-3xl font-bold text-green-400">Healthy</div>
|
|
|
+
|
|
|
+ <!-- System Info Grid -->
|
|
|
+ <template x-if="info">
|
|
|
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
|
+ <!-- Node Status -->
|
|
|
+ <div class="bg-dark-800 p-6 rounded-2xl border border-dark-700">
|
|
|
+ <h4 class="text-slate-400 text-sm font-bold uppercase mb-4">Node Status</h4>
|
|
|
+ <div class="space-y-2">
|
|
|
+ <div class="flex justify-between">
|
|
|
+ <span class="text-slate-500">ID</span>
|
|
|
+ <span class="text-white font-mono" x-text="info.node.id"></span>
|
|
|
+ </div>
|
|
|
+ <div class="flex justify-between">
|
|
|
+ <span class="text-slate-500">State</span>
|
|
|
+ <span class="font-bold" :class="info.node.state === 'Leader' ? 'text-yellow-400' : 'text-blue-400'" x-text="info.node.state"></span>
|
|
|
+ </div>
|
|
|
+ <div class="flex justify-between">
|
|
|
+ <span class="text-slate-500">Term</span>
|
|
|
+ <span class="text-white font-mono" x-text="info.node.term"></span>
|
|
|
+ </div>
|
|
|
+ <div class="flex justify-between">
|
|
|
+ <span class="text-slate-500">Health</span>
|
|
|
+ <span class="" :class="info.node.healthy ? 'text-green-400' : 'text-red-400'" x-text="info.node.healthy ? 'Healthy' : 'Unhealthy'"></span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Storage -->
|
|
|
+ <div class="bg-dark-800 p-6 rounded-2xl border border-dark-700">
|
|
|
+ <h4 class="text-slate-400 text-sm font-bold uppercase mb-4">Storage</h4>
|
|
|
+ <div class="space-y-2">
|
|
|
+ <div class="flex justify-between">
|
|
|
+ <span class="text-slate-500">DB Size</span>
|
|
|
+ <span class="text-white font-mono" x-text="(info.storage.db_size / 1024).toFixed(2) + ' KB'"></span>
|
|
|
+ </div>
|
|
|
+ <div class="flex justify-between">
|
|
|
+ <span class="text-slate-500">Log Size</span>
|
|
|
+ <span class="text-white font-mono" x-text="(info.storage.log_size / 1024).toFixed(2) + ' KB'"></span>
|
|
|
+ </div>
|
|
|
+ <div class="flex justify-between">
|
|
|
+ <span class="text-slate-500">Mem Alloc</span>
|
|
|
+ <span class="text-white font-mono" x-text="(info.storage.mem_alloc / 1024 / 1024).toFixed(2) + ' MB'"></span>
|
|
|
+ </div>
|
|
|
+ <div class="flex justify-between">
|
|
|
+ <span class="text-slate-500">GC Count</span>
|
|
|
+ <span class="text-white font-mono" x-text="info.storage.num_gc"></span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Consensus -->
|
|
|
+ <div class="bg-dark-800 p-6 rounded-2xl border border-dark-700">
|
|
|
+ <h4 class="text-slate-400 text-sm font-bold uppercase mb-4">Consensus</h4>
|
|
|
+ <div class="space-y-2">
|
|
|
+ <div class="flex justify-between">
|
|
|
+ <span class="text-slate-500">Commit Idx</span>
|
|
|
+ <span class="text-white font-mono" x-text="info.indices.commit_index"></span>
|
|
|
+ </div>
|
|
|
+ <div class="flex justify-between">
|
|
|
+ <span class="text-slate-500">Applied Idx</span>
|
|
|
+ <span class="text-white font-mono" x-text="info.indices.applied_index"></span>
|
|
|
+ </div>
|
|
|
+ <div class="flex justify-between">
|
|
|
+ <span class="text-slate-500">Last Log</span>
|
|
|
+ <span class="text-white font-mono" x-text="info.indices.last_log_index"></span>
|
|
|
+ </div>
|
|
|
+ <div class="flex justify-between">
|
|
|
+ <span class="text-slate-500">DB Applied</span>
|
|
|
+ <span class="text-white font-mono" x-text="info.indices.db_applied"></span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Cluster -->
|
|
|
+ <div class="bg-dark-800 p-6 rounded-2xl border border-dark-700 overflow-hidden">
|
|
|
+ <h4 class="text-slate-400 text-sm font-bold uppercase mb-4">Cluster (<span x-text="info.cluster_size"></span>)</h4>
|
|
|
+ <div class="space-y-2 max-h-32 overflow-y-auto">
|
|
|
+ <template x-for="(addr, id) in info.cluster" :key="id">
|
|
|
+ <div class="flex justify-between items-center text-sm">
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <div class="w-2 h-2 rounded-full" :class="id === info.node.leader ? 'bg-yellow-400' : 'bg-green-400'"></div>
|
|
|
+ <span class="text-white font-mono" x-text="id"></span>
|
|
|
+ </div>
|
|
|
+ <span class="text-slate-500 text-xs" x-text="addr"></span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- <div class="bg-dark-800 p-6 rounded-2xl border border-dark-700">
|
|
|
- <div class="text-slate-400 mb-2">Keys Checked</div>
|
|
|
- <div class="text-3xl font-bold text-blue-400" x-text="kvHistory.length"></div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- Loading State -->
|
|
|
+ <template x-if="!info">
|
|
|
+ <div class="text-center p-12 bg-dark-800 rounded-2xl border border-dark-700 animate-pulse">
|
|
|
+ <div class="text-slate-500">Loading Node Info...</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
+ </template>
|
|
|
|
|
|
<div class="bg-dark-800 rounded-2xl border border-dark-700 p-6">
|
|
|
- <h3 class="text-lg font-bold text-white mb-4">Quick Command</h3>
|
|
|
+ <h3 class="text-lg font-bold text-white mb-4">Web CLI</h3>
|
|
|
+ <div class="mb-4 text-sm text-slate-400 bg-dark-900/50 p-3 rounded-lg border border-dark-700 font-mono leading-relaxed">
|
|
|
+ <div class="flex flex-wrap gap-x-6 gap-y-2">
|
|
|
+ <span><span class="text-brand-500">GET</span> <key></span>
|
|
|
+ <span><span class="text-brand-500">SET</span> <key> <val></span>
|
|
|
+ <span><span class="text-brand-500">DEL</span> <key></span>
|
|
|
+ <span><span class="text-brand-500">SEARCH</span> <pattern></span>
|
|
|
+ <span><span class="text-brand-500">COUNT</span> <pattern></span>
|
|
|
+ <span><span class="text-brand-500">INFO</span></span>
|
|
|
+ <span><span class="text-brand-500">HELP</span></span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
<div class="flex gap-2">
|
|
|
- <input type="text" x-model="quickCmd" placeholder="GET system.config" class="flex-1 bg-dark-900 border border-dark-700 rounded-lg p-3 text-white outline-none focus:border-brand-500">
|
|
|
+ <input type="text" x-model="quickCmd" @keyup.enter="runQuickCmd" placeholder="Type command (e.g. HELP)..." class="flex-1 bg-dark-900 border border-dark-700 rounded-lg p-3 text-white outline-none focus:border-brand-500 font-mono">
|
|
|
<button @click="runQuickCmd" class="bg-brand-600 px-6 rounded-lg text-white font-bold hover:bg-brand-500">Run</button>
|
|
|
</div>
|
|
|
- <pre x-show="quickResult" class="mt-4 bg-black/30 p-4 rounded-lg text-sm text-green-400 font-mono" x-text="quickResult"></pre>
|
|
|
+ <div x-show="quickResult" class="mt-4 bg-black/50 p-4 rounded-lg border border-dark-700 overflow-x-auto">
|
|
|
+ <pre class="text-sm text-green-400 font-mono whitespace-pre-wrap" x-text="quickResult"></pre>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
@@ -236,6 +331,9 @@
|
|
|
</template>
|
|
|
<div x-show="(r.permissions || []).length > 3" class="text-xs text-slate-500 italic">...and more</div>
|
|
|
</div>
|
|
|
+ <button @click="openAddPermModal(r.name)" class="w-full py-2 bg-dark-700 hover:bg-dark-600 text-sm text-white rounded-lg transition">
|
|
|
+ Add Permission
|
|
|
+ </button>
|
|
|
</div>
|
|
|
</template>
|
|
|
</div>
|
|
|
@@ -276,6 +374,37 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
+ <!-- Add Permission Modal -->
|
|
|
+ <div x-show="showAddPermModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm" style="display: none;">
|
|
|
+ <div class="bg-dark-800 p-6 rounded-2xl w-full max-w-md border border-dark-700">
|
|
|
+ <h3 class="text-xl font-bold text-white mb-4">Add Permission to <span x-text="selectedRole" class="text-brand-500"></span></h3>
|
|
|
+ <div class="space-y-4">
|
|
|
+ <div>
|
|
|
+ <label class="block text-sm font-medium mb-2 text-slate-400">Key Pattern</label>
|
|
|
+ <input x-model="newPerm.pattern" type="text" placeholder="e.g. user.* or *" class="w-full bg-dark-900 border border-dark-700 rounded-lg p-3 text-white outline-none focus:border-brand-500">
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <label class="block text-sm font-medium mb-2 text-slate-400">Actions</label>
|
|
|
+ <div class="flex gap-4 p-3 bg-dark-900 rounded-lg border border-dark-700">
|
|
|
+ <label class="flex items-center gap-2 text-white cursor-pointer">
|
|
|
+ <input type="checkbox" value="read" x-model="newPerm.actions" class="accent-brand-500"> Read
|
|
|
+ </label>
|
|
|
+ <label class="flex items-center gap-2 text-white cursor-pointer">
|
|
|
+ <input type="checkbox" value="write" x-model="newPerm.actions" class="accent-brand-500"> Write
|
|
|
+ </label>
|
|
|
+ <label class="flex items-center gap-2 text-white cursor-pointer">
|
|
|
+ <input type="checkbox" value="admin" x-model="newPerm.actions" class="accent-brand-500"> Admin
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="flex gap-3 pt-2">
|
|
|
+ <button @click="addPermission" class="flex-1 bg-brand-600 text-white py-2 rounded-lg font-bold hover:bg-brand-500">Add</button>
|
|
|
+ <button @click="showAddPermModal = false" class="flex-1 bg-dark-700 text-white py-2 rounded-lg font-bold hover:bg-dark-600">Cancel</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<script>
|
|
|
function app() {
|
|
|
return {
|
|
|
@@ -293,6 +422,7 @@
|
|
|
users: [],
|
|
|
roles: [],
|
|
|
kvHistory: [], // Track KV ops
|
|
|
+ info: null, // System Info
|
|
|
|
|
|
// KV Op
|
|
|
kvOp: 'GET',
|
|
|
@@ -311,10 +441,22 @@
|
|
|
showAddRoleModal: false,
|
|
|
newRoleName: '',
|
|
|
|
|
|
+ // Add Permission Modal
|
|
|
+ showAddPermModal: false,
|
|
|
+ selectedRole: '',
|
|
|
+ newPerm: { pattern: '', actions: [] },
|
|
|
+
|
|
|
init() {
|
|
|
if (this.token) {
|
|
|
this.isLoggedIn = true;
|
|
|
this.fetchUsers();
|
|
|
+ this.fetchInfo();
|
|
|
+ // Refresh info periodically
|
|
|
+ setInterval(() => {
|
|
|
+ if (this.isLoggedIn && this.tab === 'dashboard') {
|
|
|
+ this.fetchInfo();
|
|
|
+ }
|
|
|
+ }, 2000);
|
|
|
}
|
|
|
},
|
|
|
|
|
|
@@ -330,7 +472,7 @@
|
|
|
});
|
|
|
const json = await res.json();
|
|
|
if (json.status === 'error') {
|
|
|
- if (json.error === 'Session Expired') {
|
|
|
+ if (json.error === 'Session Expired' || json.error.startsWith('Connection failed')) {
|
|
|
this.logout();
|
|
|
}
|
|
|
throw new Error(json.error);
|
|
|
@@ -380,17 +522,28 @@
|
|
|
try {
|
|
|
this.users = await this.api('USER_LIST');
|
|
|
if (typeof this.users === 'string') {
|
|
|
- // If proxy didn't parse JSON (edge case), try parse
|
|
|
this.users = JSON.parse(this.users);
|
|
|
}
|
|
|
} catch (e) {
|
|
|
console.error("Failed to fetch users", e);
|
|
|
- // Show error if it's "unknown command" suggesting outdated server
|
|
|
- if (e.message.includes("unknown command")) {
|
|
|
- alert("Server error: USER_LIST command not found. Please restart your Raft node to apply latest changes.");
|
|
|
- } else {
|
|
|
- this.errorMsg = "Failed to load users: " + e.message;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async fetchInfo() {
|
|
|
+ try {
|
|
|
+ const res = await this.api('INFO');
|
|
|
+ // TCP returns "OK {json}" or just {json} via proxy parsing?
|
|
|
+ // api() helper returns json.data.
|
|
|
+ // If TCP returns "OK <json>", proxy parses it if it looks like JSON.
|
|
|
+ // Our INFO returns "OK <json>".
|
|
|
+ this.info = res;
|
|
|
+ if (typeof this.info === 'string') {
|
|
|
+ try {
|
|
|
+ this.info = JSON.parse(this.info);
|
|
|
+ } catch(e) { /* ignore */ }
|
|
|
}
|
|
|
+ } catch (e) {
|
|
|
+ console.error("Failed to fetch info", e);
|
|
|
}
|
|
|
},
|
|
|
|
|
|
@@ -456,6 +609,28 @@
|
|
|
}
|
|
|
},
|
|
|
|
|
|
+ openAddPermModal(roleName) {
|
|
|
+ this.selectedRole = roleName;
|
|
|
+ this.newPerm = { pattern: '', actions: [] };
|
|
|
+ this.showAddPermModal = true;
|
|
|
+ },
|
|
|
+
|
|
|
+ async addPermission() {
|
|
|
+ if (!this.newPerm.pattern || this.newPerm.actions.length === 0) {
|
|
|
+ alert("Please provide pattern and at least one action");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ // Command: ROLE_PERMISSION_ADD <role> <pattern> <actions>
|
|
|
+ const actionsStr = this.newPerm.actions.join(',');
|
|
|
+ await this.api('ROLE_PERMISSION_ADD', [this.selectedRole, this.newPerm.pattern, actionsStr]);
|
|
|
+ this.showAddPermModal = false;
|
|
|
+ this.fetchRoles();
|
|
|
+ } catch (e) {
|
|
|
+ alert(e.message);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
async unlockUser(username) {
|
|
|
if(!confirm("Unlock user " + username + "?")) return;
|
|
|
try {
|