Web Interface for Tags #104244

Merged
Sybren A. Stüvel merged 30 commits from Evelinealy/flamenco:tag-interface into main 2023-09-04 13:06:10 +02:00
Showing only changes of commit 77a08c4254 - Show all commits

View File

@ -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">
<button @click="fetchTags">Refresh</button> <div class="btn-bar">
<button @click="deleteTag" :disabled="tags.length === 0"> <button @click="fetchTags">Refresh</button>
Delete Tag <button @click="deleteTag" :disabled="!selectedTag">Delete Tag</button>

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.

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.
</button> </div>
</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 index = this.tags.findIndex(
(tag) => tag.id === this.selectedTag.id
);
if (index !== -1) {
const api = new WorkerMgtApi(getAPIClient());
api const api = new WorkerMgtApi(getAPIClient());
.deleteWorkerTag(this.selectedTag.id) api
.then(() => { .deleteWorkerTag(this.selectedTag.id)
.then(() => {
const index = this.tags.findIndex(
(tag) => tag.id === this.selectedTag.id
);
if (index !== -1) {
this.tags.splice(index, 1); this.tags.splice(index, 1);
this.selectedTag = null; }
})
.catch((error) => { this.selectedTag = null;
const errorMsg = JSON.stringify(error); this.tabulator.setData(this.tags);
useNotifs().add(`Error: ${errorMsg}`); })
}); .catch((error) => {
} const errorMsg = JSON.stringify(error);
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;
}, },
}, },