Web Interface for Tags #104244
@ -9,6 +9,9 @@
|
|||||||
<li>
|
<li>
|
||||||
<router-link :to="{ name: 'workers' }">Workers</router-link>
|
<router-link :to="{ name: 'workers' }">Workers</router-link>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<router-link :to="{ name: 'tags' }">Tags</router-link>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<router-link :to="{ name: 'last-rendered' }">Last Rendered</router-link>
|
<router-link :to="{ name: 'last-rendered' }">Last Rendered</router-link>
|
||||||
</li>
|
</li>
|
||||||
|
@ -20,6 +20,12 @@ const router = createRouter({
|
|||||||
component: () => import('../views/WorkersView.vue'),
|
component: () => import('../views/WorkersView.vue'),
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/tags',
|
||||||
|
name: 'tags',
|
||||||
|
component: () => import('../views/TagsView.vue'),
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/last-rendered',
|
path: '/last-rendered',
|
||||||
name: 'last-rendered',
|
name: 'last-rendered',
|
||||||
|
199
web/app/src/views/TagsView.vue
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
<template>
|
||||||
|
<div class="col col-workers-list">
|
||||||
|
<h2 class="column-title">Tag Details</h2>
|
||||||
|
|
||||||
|
<div class="action-buttons btn-bar-group">
|
||||||
|
<div class="btn-bar">
|
||||||
|
<button @click="fetchTags">Refresh</button>
|
||||||
|
<button @click="deleteTag" :disabled="!selectedTag">Delete Tag</button>
|
||||||
|
|||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-buttons btn-bar">
|
||||||
|
<form @submit="createTag">
|
||||||
|
<div class="create-tag-container">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="newtagname"
|
||||||
|
v-model="newTagName"
|
||||||
|
placeholder="New Tag Name"
|
||||||
|
class="create-tag-input"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
id="submit-button"
|
||||||
|
type="submit"
|
||||||
|
:disabled="newTagName.trim() === ''"
|
||||||
|
>
|
||||||
|
Create Tag
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
Sybren A. Stüvel
commented
There shouldn't be any need to import this CSS file here. Which problem is this solving? There shouldn't be any need to import this CSS file here. Which problem is this solving?
|
|||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tag-table-container"></div>
|
||||||
|
</div>
|
||||||
|
<footer class="app-footer"></footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.create-tag-container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.create-tag-input {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 10px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { TabulatorFull as Tabulator } from "tabulator-tables";
|
||||||
|
import { useWorkers } from "@/stores/workers";
|
||||||
Sybren A. Stüvel
commented
`activeRowIndex` is only set, but never read. That means it can be removed.
|
|||||||
|
import { useNotifs } from "@/stores/notifications";
|
||||||
|
import { WorkerMgtApi } from "@/manager-api";
|
||||||
|
import { WorkerTag } from "@/manager-api";
|
||||||
|
import { getAPIClient } from "@/api-client";
|
||||||
|
import TabItem from "@/components/TabItem.vue";
|
||||||
|
import TabsWrapper from "@/components/TabsWrapper.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
Sybren A. Stüvel
commented
This line was just for debugging, so it can be removed now. This line was just for debugging, so it can be removed now.
|
|||||||
|
TabItem,
|
||||||
|
TabsWrapper,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tags: [],
|
||||||
|
selectedTag: null,
|
||||||
|
newTagName: "",
|
||||||
|
workers: useWorkers(),
|
||||||
|
activeRowIndex: -1,
|
||||||
|
};
|
||||||
|
},
|
||||||
Sybren A. Stüvel
commented
I think this is a copy-paste leftover of the jobs table; tags are immediately deleted, whereas jobs are first marked as 'deletion requested' and then deleted by a background process. This line can be removed. I think this is a copy-paste leftover of the jobs table; tags are immediately deleted, whereas jobs are first marked as 'deletion requested' and then deleted by a background process. This line can be removed.
|
|||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.fetchTags();
|
||||||
|
|
||||||
|
const tag_options = {
|
||||||
|
columns: [
|
||||||
|
{ title: "Name", field: "name", sorter: "string", editor: "input" },
|
||||||
|
{
|
||||||
|
title: "Description",
|
||||||
|
field: "description",
|
||||||
|
sorter: "string",
|
||||||
|
editor: "input",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
layout: "fitData",
|
||||||
|
layoutColumnsOnNewData: true,
|
||||||
Sybren A. Stüvel
commented
This function is not called, and thus should be removed. This function is not called, and thus should be removed.
|
|||||||
|
height: "82%",
|
||||||
|
selectable: true,
|
||||||
|
};
|
||||||
|
|
||||||
Sybren A. Stüvel
commented
This function is not called, and thus should be removed. This function is not called, and thus should be removed.
|
|||||||
|
this.tabulator = new Tabulator("#tag-table-container", tag_options);
|
||||||
|
this.tabulator.on("rowClick", this.onRowClick);
|
||||||
|
this.tabulator.on("tableBuilt", () => {
|
||||||
|
this.fetchTags();
|
||||||
|
});
|
||||||
|
this.tabulator.on("cellEdited", (cell) => {
|
||||||
|
const editedTag = cell.getRow().getData();
|
||||||
|
this.updateTagInAPI(editedTag);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
_onTableBuilt() {
|
||||||
|
this.fetchTags();
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchTags() {
|
||||||
|
this.workers
|
||||||
|
.refreshTags()
|
||||||
|
.then(() => {
|
||||||
|
this.tags = this.workers.tags;
|
||||||
|
this.tabulator.setData(this.tags);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const errorMsg = JSON.stringify(error);
|
||||||
Sybren A. Stüvel
commented
I think it's also nice to clear the input field once the tag has been succesfully created. I think it's also nice to clear the input field once the tag has been succesfully created.
|
|||||||
|
useNotifs().add(`Error: ${errorMsg}`);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
createTag(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const api = new WorkerMgtApi(getAPIClient());
|
||||||
|
const newTag = new WorkerTag(this.newTagName);
|
||||||
|
|
||||||
|
newTag.description = "Default Description...";
|
||||||
|
|
||||||
|
api
|
||||||
|
.createWorkerTag(newTag)
|
||||||
|
.then(() => {
|
||||||
|
this.fetchTags(); // Refresh table data
|
||||||
|
this.newTagName = "";
|
||||||
Sybren A. Stüvel
commented
Instead of trying to find the just-deleted tag, just call This is all temporary code, and that it should be removed once the SocketIO broadcasting of tag changes is implemented. Instead of trying to find the just-deleted tag, just call `this.fetchTags()`.
This is all temporary code, and that it should be removed once the SocketIO broadcasting of tag changes is implemented.
|
|||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const errorMsg = JSON.stringify(error);
|
||||||
|
useNotifs().add(`Error: ${errorMsg}`);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateTagInAPI(tag) {
|
||||||
|
const { id: tagId, ...updatedTagData } = tag;
|
||||||
|
const api = new WorkerMgtApi(getAPIClient());
|
||||||
|
|
||||||
|
api
|
||||||
|
.updateWorkerTag(tagId, updatedTagData)
|
||||||
|
.then(() => {
|
||||||
|
// Update the local state with the edited data without requiring a page refresh
|
||||||
|
this.tags = this.tags.map((tag) => {
|
||||||
|
if (tag.id === tagId) {
|
||||||
|
return { ...tag, ...updatedTagData };
|
||||||
|
}
|
||||||
|
return tag;
|
||||||
|
});
|
||||||
Sybren A. Stüvel
commented
Not sure why this is implemented in a separate function, it could just all be part of the Not sure why this is implemented in a separate function, it could just all be part of the `onRowClick` event handler.
|
|||||||
|
console.log("Tag updated successfully");
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
Sybren A. Stüvel
commented
This alone is not enough to update all the rows in the table. When I click one tag, and then another, both appear as if they're selected. You probably need to force a redraw of the table somehow (there's a Tabulator API call for this). This alone is not enough to update all the rows in the table. When I click one tag, and then another, both appear as if they're selected. You probably need to force a redraw of the table somehow (there's a Tabulator API call for this).
|
|||||||
|
const errorMsg = JSON.stringify(error);
|
||||||
|
useNotifs().add(`Error: ${errorMsg}`);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteTag() {
|
||||||
|
if (!this.selectedTag) {
|
||||||
Sybren A. Stüvel
commented
`isSelected` is not used, so it can be removed. Also I don't see it working, given that it uses an undefined variable `tag`.
|
|||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = new WorkerMgtApi(getAPIClient());
|
||||||
|
api
|
||||||
|
.deleteWorkerTag(this.selectedTag.id)
|
||||||
|
.then(() => {
|
||||||
|
this.selectedTag = null;
|
||||||
|
this.tabulator.setData(this.tags);
|
||||||
|
|
||||||
|
this.fetchTags();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const errorMsg = JSON.stringify(error);
|
||||||
|
useNotifs().add(`Error: ${errorMsg}`);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onRowClick(event, row) {
|
||||||
|
const tag = row.getData();
|
||||||
|
const rowIndex = row.getIndex();
|
||||||
|
|
||||||
|
this.tabulator.deselectRow();
|
||||||
|
this.tabulator.selectRow(rowIndex);
|
||||||
|
|
||||||
|
this.selectedTag = tag;
|
||||||
|
this.activeRowIndex = rowIndex;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
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.