Private
Public Access
1
0

feat: implement server tag management with individual fields and update routes

This commit is contained in:
2025-12-18 21:09:45 -05:00
parent db04b8a6c3
commit b0f7c7f20d
6 changed files with 167 additions and 217 deletions

View File

@@ -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>

View File

@@ -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