feat: implement server tag management with individual fields and update routes
This commit is contained in:
@@ -1225,18 +1225,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Server Tags Manager - Proxmox-style tags with color generation
|
||||
function serverTagsManager({serverId, initialTags}) {
|
||||
// Server Tags Manager - Simple tags stored in database
|
||||
function serverTagsManager({serverId, tag1, tag1color, tag2, tag2color, tag3, tag3color}) {
|
||||
return {
|
||||
serverId: serverId,
|
||||
show: false,
|
||||
saving: false,
|
||||
tags: initialTags || [],
|
||||
editTags: [],
|
||||
colorPickerIndex: null,
|
||||
tag1: tag1 || '',
|
||||
tag1color: tag1color || '#3498db',
|
||||
tag2: tag2 || '',
|
||||
tag2color: tag2color || '#2ecc71',
|
||||
tag3: tag3 || '',
|
||||
tag3color: tag3color || '#e74c3c',
|
||||
colorPickerFor: null,
|
||||
message: '',
|
||||
messageType: 'success',
|
||||
// Proxmox-style tag colors
|
||||
tagColors: [
|
||||
'#e74c3c', '#e67e22', '#f1c40f', '#2ecc71', '#1abc9c', '#3498db',
|
||||
'#9b59b6', '#e91e63', '#00bcd4', '#8bc34a', '#ff5722', '#607d8b',
|
||||
@@ -1245,74 +1248,55 @@
|
||||
openModal() {
|
||||
this.show = true;
|
||||
this.message = '';
|
||||
this.colorPickerIndex = null;
|
||||
// Clone tags for editing
|
||||
this.editTags = JSON.parse(JSON.stringify(this.tags));
|
||||
this.colorPickerFor = null;
|
||||
},
|
||||
closeModal() {
|
||||
this.show = false;
|
||||
this.colorPickerIndex = null;
|
||||
this.colorPickerFor = null;
|
||||
},
|
||||
addTag() {
|
||||
if (this.editTags.length < 3) {
|
||||
this.editTags.push({
|
||||
text: '',
|
||||
color: this.generateTagColor('')
|
||||
});
|
||||
}
|
||||
selectColorFor(tagNum) {
|
||||
this.colorPickerFor = this.colorPickerFor === tagNum ? null : tagNum;
|
||||
},
|
||||
removeTag(index) {
|
||||
this.editTags.splice(index, 1);
|
||||
if (this.colorPickerIndex === index) {
|
||||
this.colorPickerIndex = null;
|
||||
} else if (this.colorPickerIndex > index) {
|
||||
this.colorPickerIndex--;
|
||||
}
|
||||
setColor(tagNum, color) {
|
||||
if (tagNum === 1) this.tag1color = color;
|
||||
else if (tagNum === 2) this.tag2color = color;
|
||||
else if (tagNum === 3) this.tag3color = color;
|
||||
this.colorPickerFor = null;
|
||||
},
|
||||
selectColorFor(index) {
|
||||
this.colorPickerIndex = this.colorPickerIndex === index ? null : index;
|
||||
},
|
||||
setTagColor(index, color) {
|
||||
if (this.editTags[index]) {
|
||||
this.editTags[index].color = color;
|
||||
}
|
||||
this.colorPickerIndex = null;
|
||||
},
|
||||
generateTagColor(text) {
|
||||
// Same algorithm as backend for consistency
|
||||
let hash = 0;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
hash = text.charCodeAt(i) + ((hash << 5) - hash);
|
||||
hash = hash & hash;
|
||||
}
|
||||
const index = Math.abs(hash) % this.tagColors.length;
|
||||
return this.tagColors[index];
|
||||
getColor(tagNum) {
|
||||
if (tagNum === 1) return this.tag1color;
|
||||
if (tagNum === 2) return this.tag2color;
|
||||
if (tagNum === 3) return this.tag3color;
|
||||
return '#3498db';
|
||||
},
|
||||
saveTags() {
|
||||
this.saving = true;
|
||||
this.message = '';
|
||||
|
||||
// Filter out empty tags
|
||||
const validTags = this.editTags.filter(tag => tag.text && tag.text.trim());
|
||||
|
||||
fetch(`/proxmox/server/${this.serverId}/tags`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content
|
||||
},
|
||||
body: JSON.stringify({ tags: validTags })
|
||||
body: JSON.stringify({
|
||||
tag1: this.tag1.trim(),
|
||||
tag1color: this.tag1color,
|
||||
tag2: this.tag2.trim(),
|
||||
tag2color: this.tag2color,
|
||||
tag3: this.tag3.trim(),
|
||||
tag3color: this.tag3color
|
||||
})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
this.saving = false;
|
||||
if (data.success) {
|
||||
this.tags = data.tags;
|
||||
this.message = 'Tags saved!';
|
||||
this.messageType = 'success';
|
||||
setTimeout(() => { this.closeModal(); location.reload(); }, 1000);
|
||||
} else {
|
||||
this.message = 'Failed to save tags';
|
||||
this.message = data.error || 'Failed to save tags';
|
||||
this.messageType = 'error';
|
||||
}
|
||||
})
|
||||
@@ -1330,15 +1314,34 @@
|
||||
<!-- Server Tags Component -->
|
||||
<div x-data="serverTagsManager({
|
||||
serverId: '{{ $server->id }}',
|
||||
initialTags: {{ json_encode($server->tags ?? []) }}
|
||||
tag1: '{{ $server->tag1 ?? '' }}',
|
||||
tag1color: '{{ $server->tag1color ?? '#3498db' }}',
|
||||
tag2: '{{ $server->tag2 ?? '' }}',
|
||||
tag2color: '{{ $server->tag2color ?? '#2ecc71' }}',
|
||||
tag3: '{{ $server->tag3 ?? '' }}',
|
||||
tag3color: '{{ $server->tag3color ?? '#e74c3c' }}'
|
||||
})" class="flex items-center gap-2">
|
||||
<!-- Display Tags -->
|
||||
<div class="flex items-center gap-1.5 flex-wrap">
|
||||
<template x-for="(tag, index) in tags" :key="index">
|
||||
<template x-if="tag1">
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium text-white cursor-pointer hover:opacity-80 transition-opacity"
|
||||
:style="'background-color: ' + tag.color"
|
||||
:style="'background-color: ' + tag1color"
|
||||
@click="openModal()">
|
||||
<span x-text="tag.text"></span>
|
||||
<span x-text="tag1"></span>
|
||||
</span>
|
||||
</template>
|
||||
<template x-if="tag2">
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium text-white cursor-pointer hover:opacity-80 transition-opacity"
|
||||
:style="'background-color: ' + tag2color"
|
||||
@click="openModal()">
|
||||
<span x-text="tag2"></span>
|
||||
</span>
|
||||
</template>
|
||||
<template x-if="tag3">
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium text-white cursor-pointer hover:opacity-80 transition-opacity"
|
||||
:style="'background-color: ' + tag3color"
|
||||
@click="openModal()">
|
||||
<span x-text="tag3"></span>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
@@ -1357,46 +1360,58 @@
|
||||
<x-ri-close-line class="size-5" />
|
||||
</button>
|
||||
<h3 class="text-lg font-semibold mb-4">Manage Server Tags</h3>
|
||||
<p class="text-sm text-base/60 mb-4">Add up to 3 tags to organize and identify your server. Tags are synced to Proxmox.</p>
|
||||
<p class="text-sm text-base/60 mb-4">Add up to 3 tags to organize and identify your server.</p>
|
||||
|
||||
<!-- Current Tags -->
|
||||
<!-- Tag 1 -->
|
||||
<div class="space-y-3 mb-4">
|
||||
<template x-for="(tag, index) in editTags" :key="index">
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="text"
|
||||
x-model="editTags[index].text"
|
||||
placeholder="Tag name (a-z, 0-9, -, _)"
|
||||
class="flex-1 px-3 py-2 text-sm border border-neutral rounded-lg dark:bg-background dark:border-base/20 focus:ring-2 focus:ring-primary/50 focus:border-primary"
|
||||
maxlength="32"
|
||||
@input="editTags[index].text = editTags[index].text.toLowerCase().replace(/[^a-z0-9_-]/g, '')" />
|
||||
<button @click="selectColorFor(index)"
|
||||
class="size-8 rounded-full border-2 border-white shadow-md transition-transform hover:scale-110"
|
||||
:style="'background-color: ' + editTags[index].color"
|
||||
title="Change color">
|
||||
</button>
|
||||
<button @click="removeTag(index)" class="text-base/50 hover:text-danger transition-colors" title="Remove tag">
|
||||
<x-ri-delete-bin-line class="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-base/50 w-12">Tag 1:</span>
|
||||
<input type="text" x-model="tag1" placeholder="Tag 1"
|
||||
class="flex-1 px-3 py-2 text-sm border border-neutral rounded-lg dark:bg-background dark:border-base/20 focus:ring-2 focus:ring-primary/50 focus:border-primary"
|
||||
maxlength="32" />
|
||||
<button @click="selectColorFor(1)"
|
||||
class="size-8 rounded-full border-2 border-white shadow-md transition-transform hover:scale-110"
|
||||
:style="'background-color: ' + tag1color"
|
||||
title="Change color">
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tag 2 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-base/50 w-12">Tag 2:</span>
|
||||
<input type="text" x-model="tag2" placeholder="Tag 2"
|
||||
class="flex-1 px-3 py-2 text-sm border border-neutral rounded-lg dark:bg-background dark:border-base/20 focus:ring-2 focus:ring-primary/50 focus:border-primary"
|
||||
maxlength="32" />
|
||||
<button @click="selectColorFor(2)"
|
||||
class="size-8 rounded-full border-2 border-white shadow-md transition-transform hover:scale-110"
|
||||
:style="'background-color: ' + tag2color"
|
||||
title="Change color">
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tag 3 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-base/50 w-12">Tag 3:</span>
|
||||
<input type="text" x-model="tag3" placeholder="Tag 3"
|
||||
class="flex-1 px-3 py-2 text-sm border border-neutral rounded-lg dark:bg-background dark:border-base/20 focus:ring-2 focus:ring-primary/50 focus:border-primary"
|
||||
maxlength="32" />
|
||||
<button @click="selectColorFor(3)"
|
||||
class="size-8 rounded-full border-2 border-white shadow-md transition-transform hover:scale-110"
|
||||
:style="'background-color: ' + tag3color"
|
||||
title="Change color">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Tag Button -->
|
||||
<template x-if="editTags.length < 3">
|
||||
<button @click="addTag()" class="w-full py-2 border border-dashed border-base/30 rounded-lg text-sm text-base/60 hover:border-primary hover:text-primary transition-colors mb-4">
|
||||
<x-ri-add-line class="size-4 inline" /> Add Tag
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<!-- Color Picker (shown when selecting color) -->
|
||||
<template x-if="colorPickerIndex !== null">
|
||||
<!-- Color Picker -->
|
||||
<template x-if="colorPickerFor !== null">
|
||||
<div class="mb-4 p-3 bg-background rounded-lg border border-neutral">
|
||||
<p class="text-xs text-base/60 mb-2">Select color for tag:</p>
|
||||
<p class="text-xs text-base/60 mb-2">Select color:</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<template x-for="c in tagColors" :key="c">
|
||||
<button @click="setTagColor(colorPickerIndex, c)"
|
||||
<button @click="setColor(colorPickerFor, c)"
|
||||
:style="'background-color: ' + c"
|
||||
:class="editTags[colorPickerIndex]?.color === c ? 'ring-2 ring-offset-2 ring-primary' : ''"
|
||||
:class="getColor(colorPickerFor) === c ? 'ring-2 ring-offset-2 ring-primary' : ''"
|
||||
class="size-7 rounded-full border border-white shadow-sm hover:scale-110 transition-transform">
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@@ -24,17 +24,27 @@
|
||||
@endif
|
||||
<span class="font-medium">{{ $status->name }}</span>
|
||||
|
||||
{{-- Server Tags (Proxmox-style) --}}
|
||||
@if($server->tags && count($server->tags) > 0)
|
||||
<div class="flex items-center gap-1.5 ml-2">
|
||||
@foreach($server->tags as $tag)
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium text-white"
|
||||
style="background-color: {{ $tag['color'] ?? '#6366f1' }};">
|
||||
{{ $tag['text'] }}
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
{{-- Server Tags --}}
|
||||
<div class="flex items-center gap-1.5 ml-2">
|
||||
@if($server->tag1)
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium text-white"
|
||||
style="background-color: {{ $server->tag1color ?? '#3498db' }};">
|
||||
{{ $server->tag1 }}
|
||||
</span>
|
||||
@endif
|
||||
@if($server->tag2)
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium text-white"
|
||||
style="background-color: {{ $server->tag2color ?? '#2ecc71' }};">
|
||||
{{ $server->tag2 }}
|
||||
</span>
|
||||
@endif
|
||||
@if($server->tag3)
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium text-white"
|
||||
style="background-color: {{ $server->tag3color ?? '#e74c3c' }};">
|
||||
{{ $server->tag3 }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 px-3 py-1.5 rounded-lg
|
||||
@if($status->status == 'running') text-success bg-success/20
|
||||
|
||||
Reference in New Issue
Block a user