Web Interface for Tags #104244
@ -2,11 +2,11 @@
|
|||||||
<div class="col col-workers-list">
|
<div class="col col-workers-list">
|
||||||
<h2 class="column-title">Tag Details</h2>
|
<h2 class="column-title">Tag Details</h2>
|
||||||
|
|
||||||
<div class="action-buttons">
|
<div class="action-buttons btn-bar-group">
|
||||||
|
<div class="btn-bar">
|
||||||
<button @click="fetchTags">Refresh</button>
|
<button @click="fetchTags">Refresh</button>
|
||||||
<button @click="deleteTag" :disabled="tags.length === 0">
|
<button @click="deleteTag" :disabled="!selectedTag">Delete Tag</button>
|
||||||
|
|||||||
Delete Tag
|
</div>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
@ -21,103 +21,17 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Table to display tags -->
|
<div id="tag-table-container"></div>
|
||||||
<table v-if="tags.length > 0" class="tag-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Description</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr
|
|
||||||
v-for="tag in tags"
|
|
||||||
:key="tag.name"
|
|
||||||
@click="onTagClick(tag)"
|
|
||||||
:class="{ selected: isSelected(tag) }"
|
|
||||||
>
|
|
||||||
<td>{{ tag.name }}</td>
|
|
||||||
<td>{{ tag.description }}</td>
|
|
||||||
|
|
||||||
<!-- Name editing field -->
|
|
||||||
<td>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
v-if="isSelected(tag)"
|
|
||||||
v-model="tag.name"
|
|
||||||
@blur="updateTagName(tag)"
|
|
||||||
/>
|
|
||||||
<span v-else>{{ tag.name }}</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<div v-else class="dl-no-data">
|
|
||||||
<span>No tags found.</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<footer class="app-footer"></footer>
|
<footer class="app-footer"></footer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.action-buttons {
|
@import "@/assets/base.css";
|
||||||
margin-bottom: 10px;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons button {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add some basic styling to the table */
|
|
||||||
.tag-table {
|
|
||||||
background-color: var(--table-color-background-row-odd);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
font-family: var(--font-family-mono);
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
text-align: left;
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin-top: 20px;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
text-align: left;
|
|
||||||
transform: translateZ(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-table th {
|
|
||||||
height: 24px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-table td {
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
padding: 8px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-table th {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-table tbody tr:hover {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Style for selected row */
|
|
||||||
.tag-table tbody tr.selected {
|
|
||||||
background-color: rgb(137, 130, 201);
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { TabulatorFull as Tabulator } from "tabulator-tables";
|
||||||
import { useWorkers } from "@/stores/workers";
|
import { useWorkers } from "@/stores/workers";
|
||||||
import { useNotifs } from "@/stores/notifications";
|
import { useNotifs } from "@/stores/notifications";
|
||||||
import { WorkerMgtApi } from "@/manager-api";
|
import { WorkerMgtApi } from "@/manager-api";
|
||||||
@ -137,22 +51,56 @@ export default {
|
|||||||
selectedTag: null,
|
selectedTag: null,
|
||||||
newTagName: "",
|
newTagName: "",
|
||||||
workers: useWorkers(),
|
workers: useWorkers(),
|
||||||
|
activeRowIndex: -1,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchTags();
|
this.fetchTags();
|
||||||
|
|
||||||
|
const vueComponent = this;
|
||||||
const api = new WorkerMgtApi(getAPIClient());
|
const api = new WorkerMgtApi(getAPIClient());
|
||||||
window.api = api;
|
window.api = api;
|
||||||
|
|
||||||
|
const tag_options = {
|
||||||
|
columns: [
|
||||||
|
{ title: "Name", field: "name", sorter: "string" },
|
||||||
|
{ title: "Description", field: "description", sorter: "string" },
|
||||||
|
],
|
||||||
|
rowFormatter(row) {
|
||||||
|
const data = row.getData();
|
||||||
|
const isActive = data.id === vueComponent.activeTagID;
|
||||||
|
const classList = row.getElement().classList;
|
||||||
|
classList.toggle("active-row", isActive);
|
||||||
|
classList.toggle("deletion-requested", !!data.delete_requested_at);
|
||||||
|
},
|
||||||
|
layout: "fitData",
|
||||||
|
layoutColumnsOnNewData: true,
|
||||||
|
height: "525px", // Must be set in order for the virtual DOM to function correctly.
|
||||||
|
selectable: true, // The active worker is tracked by click events, not row selection.
|
||||||
|
};
|
||||||
|
|
||||||
|
this.tabulator = new Tabulator("#tag-table-container", tag_options);
|
||||||
|
this.tabulator.on("rowClick", this.onRowClick);
|
||||||
|
this.tabulator.on("tableBuilt", () => {
|
||||||
|
this.fetchTags();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
sortData() {
|
||||||
|
const tab = this.tabulator;
|
||||||
|
tab.setSort(tab.getSorters()); // This triggers re-sorting.
|
||||||
|
},
|
||||||
|
_onTableBuilt() {
|
||||||
|
this.fetchTags();
|
||||||
|
},
|
||||||
fetchTags() {
|
fetchTags() {
|
||||||
this.workers
|
this.workers
|
||||||
.refreshTags()
|
.refreshTags()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.tags = this.workers.tags;
|
this.tags = this.workers.tags;
|
||||||
|
this.tabulator.setData(this.tags);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
const errorMsg = JSON.stringify(error);
|
const errorMsg = JSON.stringify(error);
|
||||||
@ -168,7 +116,9 @@ export default {
|
|||||||
|
|
||||||
api
|
api
|
||||||
.createWorkerTag(newTag)
|
.createWorkerTag(newTag)
|
||||||
.then(this.fetchTags)
|
.then(() => {
|
||||||
|
this.fetchTags(); // Refresh table data
|
||||||
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
const errorMsg = JSON.stringify(error);
|
const errorMsg = JSON.stringify(error);
|
||||||
useNotifs().add(`Error: ${errorMsg}`);
|
useNotifs().add(`Error: ${errorMsg}`);
|
||||||
@ -176,34 +126,40 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
deleteTag() {
|
deleteTag() {
|
||||||
if (this.tags.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this.selectedTag) {
|
if (!this.selectedTag) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Find the index of the selected tag in the tags array
|
|
||||||
|
const api = new WorkerMgtApi(getAPIClient());
|
||||||
|
api
|
||||||
|
.deleteWorkerTag(this.selectedTag.id)
|
||||||
|
.then(() => {
|
||||||
const index = this.tags.findIndex(
|
const index = this.tags.findIndex(
|
||||||
(tag) => tag.id === this.selectedTag.id
|
(tag) => tag.id === this.selectedTag.id
|
||||||
);
|
);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
const api = new WorkerMgtApi(getAPIClient());
|
|
||||||
|
|
||||||
api
|
|
||||||
.deleteWorkerTag(this.selectedTag.id)
|
|
||||||
.then(() => {
|
|
||||||
this.tags.splice(index, 1);
|
this.tags.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
this.selectedTag = null;
|
this.selectedTag = null;
|
||||||
|
this.tabulator.setData(this.tags);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
const errorMsg = JSON.stringify(error);
|
const errorMsg = JSON.stringify(error);
|
||||||
useNotifs().add(`Error: ${errorMsg}`);
|
useNotifs().add(`Error: ${errorMsg}`);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onTagClick(tag) {
|
onRowClick(event, row) {
|
||||||
this.selectedTag = tag;
|
const tag = row.getData();
|
||||||
|
this.onTagClick(tag, row.getIndex());
|
||||||
|
},
|
||||||
|
|
||||||
|
onTagClick(tag, rowIndex) {
|
||||||
|
console.log("Clicked Tag:", tag);
|
||||||
|
console.log("Selected Tag:", this.selectedTag);
|
||||||
|
this.selectedTag = this.selectedTag === tag ? null : tag;
|
||||||
|
this.activeRowIndex = rowIndex;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user
For a followup PR, I think it would be nicer to remove this button, and have a little ❌ icon behind each tag (just like in
Blocklist.vue
for removing blocklist entries).Since selection of tags is only used for deleting them, this little change would remove the entire need to select tags. It also clears up the double semantics of clicking on a tag name: it currently both selects the tag and allows renaming it.