From a78c6a73405da0b7fe9877716b1587fe2a8f8fda Mon Sep 17 00:00:00 2001 From: Jiachen Fan Date: Tue, 11 Aug 2026 10:30:21 +0800 Subject: [PATCH 1/6] feat(DIARCHERS-1650): add Project Token creation entry to My Tokens page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Project Tokens section of the My Tokens page was read-only, forcing users into each project's Settings → Tokens page to create a token. Add an inline create form (project selector + description) that reuses the existing Project.addToken() → POST /api/v1/projects/{id}/tokens, so both entries share the same backend and logic. Frontend-only; no backend change. --- .../src/components/user/UserGlobalTokens.vue | 52 ++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/dashboard-client/src/components/user/UserGlobalTokens.vue b/src/dashboard-client/src/components/user/UserGlobalTokens.vue index ecb34e58..694bebfc 100644 --- a/src/dashboard-client/src/components/user/UserGlobalTokens.vue +++ b/src/dashboard-client/src/components/user/UserGlobalTokens.vue @@ -236,13 +236,40 @@ Project Tokens - Manage individual project tokens in each project's settings + Create and manage project tokens (INFRABOX_CLI_TOKEN) + + + + + + + + + {{ p.name }} + + + + + + + + add_circle + Create project token + + +
+ You have no projects with admin rights to create tokens for. +
+
+
+
+ @@ -358,6 +385,10 @@ export default { description: '', expiresDays: 365 }, + projectTokenForm: { + projectId: '', + description: '' + }, mcpTokens: [], newMcpToken: '', pendingMcpRevoke: null, @@ -375,6 +406,10 @@ export default { return !this.form.description || this.form.description.length < 3 || !this.form.expiresDays || this.form.expiresDays < 1 || this.form.expiresDays > 3650 }, + disableProjectTokenAdd () { + return !this.projectTokenForm.projectId || + !this.projectTokenForm.description || this.projectTokenForm.description.length < 3 + }, disableMcpAdd () { return !this.mcpForm.name || this.mcpForm.name.length < 3 || !this.mcpForm.expiresDays || this.mcpForm.expiresDays < 1 || this.mcpForm.expiresDays > 365 @@ -458,6 +493,21 @@ export default { this.$refs['revokeDialog'].open() }, + createProjectToken () { + if (this.disableProjectTokenAdd) return + const project = this.adminProjects.find(p => p.id === this.projectTokenForm.projectId) + if (!project) return + // Reuse the same Project.addToken() used by project settings — it posts to + // POST projects/{id}/tokens and reloads that project's token list on success. + project.addToken(this.projectTokenForm.description) + .then((token) => { + if (!token) return + this.newToken = token + this.$refs['tokenDialog'].open() + this.projectTokenForm.description = '' + }) + }, + onRevokeClose (type) { if (type !== 'ok' || !this.pendingRevoke) { this.pendingRevoke = null From fbbc1b0db7f11b33600c58a4326d616a9d83a43f Mon Sep 17 00:00:00 2001 From: Jiachen Fan Date: Wed, 12 Aug 2026 10:50:25 +0800 Subject: [PATCH 2/6] fix(DIARCHERS-1650): give project-select options an opaque background The project dropdown in the new Project Token create form rendered with a transparent menu, so option text overlapped the "Project" label. Add the global bg-white class to md-option (matching every other md-select in the dashboard) and name/id attributes for consistency. The dropdown's built-in max-height + scroll handles long project lists. --- src/dashboard-client/src/components/user/UserGlobalTokens.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dashboard-client/src/components/user/UserGlobalTokens.vue b/src/dashboard-client/src/components/user/UserGlobalTokens.vue index 694bebfc..fddb54a7 100644 --- a/src/dashboard-client/src/components/user/UserGlobalTokens.vue +++ b/src/dashboard-client/src/components/user/UserGlobalTokens.vue @@ -250,8 +250,8 @@ - - {{ p.name }} + + {{ p.name }} From d3c322065e4cb4018cc8876033e0e01ecb9cb268 Mon Sep 17 00:00:00 2001 From: Jiachen Fan Date: Wed, 12 Aug 2026 14:15:52 +0800 Subject: [PATCH 3/6] fix(DIARCHERS-1650): load project tokens after async projects arrive The project token list depends on store.state.projects, which is populated asynchronously. Opening My Tokens directly (e.g. on refresh) ran created() before projects loaded, so the list stayed empty until an unrelated action forced a re-render. Extract the load into loadProjectTokens() and watch adminProjects.length so the list populates once projects arrive, guarded by projectTokensLoaded to avoid reloading on later changes. --- .../src/components/user/UserGlobalTokens.vue | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/dashboard-client/src/components/user/UserGlobalTokens.vue b/src/dashboard-client/src/components/user/UserGlobalTokens.vue index fddb54a7..2e244a12 100644 --- a/src/dashboard-client/src/components/user/UserGlobalTokens.vue +++ b/src/dashboard-client/src/components/user/UserGlobalTokens.vue @@ -381,6 +381,7 @@ export default { accessLog: [], logLoading: false, projectTokensLoading: false, + projectTokensLoaded: false, form: { description: '', expiresDays: 365 @@ -441,15 +442,31 @@ export default { this.mcpTokens = tokens }).catch(() => {}) - const adminProjects = this.$store.state.projects.filter(p => p.userHasAdminRights()) - if (adminProjects.length > 0) { - this.projectTokensLoading = true - Promise.all(adminProjects.map(p => p._loadTokens())) - .finally(() => { this.projectTokensLoading = false }) + this.loadProjectTokens() + }, + + watch: { + // store.state.projects is populated asynchronously. If this page is opened + // directly (e.g. on refresh) before projects have loaded, created() sees an + // empty list and never loads project tokens. Re-run the load once projects + // arrive. projectTokensLoaded guards against reloading on later changes. + 'adminProjects.length' (len) { + if (len > 0 && !this.projectTokensLoaded) { + this.loadProjectTokens() + } } }, methods: { + loadProjectTokens () { + const adminProjects = this.adminProjects + if (adminProjects.length === 0) return + this.projectTokensLoaded = true + this.projectTokensLoading = true + Promise.all(adminProjects.map(p => p._loadTokens())) + .finally(() => { this.projectTokensLoading = false }) + }, + formatDate (v) { return v ? moment(v).format('YYYY-MM-DD HH:mm:ss') : '-' }, From 1d0f2cbb0f3b686ad2aa2171231e4be7585b4206 Mon Sep 17 00:00:00 2001 From: Jiachen Fan Date: Wed, 12 Aug 2026 14:26:25 +0800 Subject: [PATCH 4/6] fix(DIARCHERS-1650): track loaded project ids and fetch late arrivals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review findings on the project-token loading path: - The permanent projectTokensLoaded gate blocked the watcher's catch-up, so admin projects arriving in a later store commit never got their tokens loaded. Track loaded project ids instead and fetch only not-yet-loaded projects, so late arrivals are picked up while avoiding duplicate GETs. - Use _reloadTokens() (returns a real Promise) rather than _loadTokens() (returns undefined on cache hit), so projectTokensLoading stays on until the GETs actually complete — no premature "No project tokens found." flash — and the list is fresh on every visit. - Hide the create form (not just the note) when the user has no admin-rights projects. --- .../src/components/user/UserGlobalTokens.vue | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/dashboard-client/src/components/user/UserGlobalTokens.vue b/src/dashboard-client/src/components/user/UserGlobalTokens.vue index 2e244a12..bf1b8268 100644 --- a/src/dashboard-client/src/components/user/UserGlobalTokens.vue +++ b/src/dashboard-client/src/components/user/UserGlobalTokens.vue @@ -247,7 +247,7 @@ - + @@ -381,7 +381,7 @@ export default { accessLog: [], logLoading: false, projectTokensLoading: false, - projectTokensLoaded: false, + loadedProjectIds: [], form: { description: '', expiresDays: 365 @@ -446,24 +446,28 @@ export default { }, watch: { - // store.state.projects is populated asynchronously. If this page is opened - // directly (e.g. on refresh) before projects have loaded, created() sees an - // empty list and never loads project tokens. Re-run the load once projects - // arrive. projectTokensLoaded guards against reloading on later changes. - 'adminProjects.length' (len) { - if (len > 0 && !this.projectTokensLoaded) { - this.loadProjectTokens() - } + // store.state.projects is populated asynchronously and can grow across + // multiple commits (e.g. a single-project load followed by the full list). + // Re-run the loader whenever the admin project set grows; loadProjectTokens() + // only fetches projects it hasn't fetched yet, so late arrivals are picked up. + 'adminProjects.length' () { + this.loadProjectTokens() } }, methods: { loadProjectTokens () { - const adminProjects = this.adminProjects - if (adminProjects.length === 0) return - this.projectTokensLoaded = true + // Only fetch projects we haven't fetched yet (tracked by id), so admin + // projects arriving in a later store commit still get loaded. Use + // _reloadTokens() (returns a real Promise) instead of _loadTokens() so + // the loading flag stays on until the GETs actually complete and the + // data is fresh on every visit. + const pending = this.adminProjects.filter(p => this.loadedProjectIds.indexOf(p.id) === -1) + if (pending.length === 0) return + pending.forEach(p => this.loadedProjectIds.push(p.id)) this.projectTokensLoading = true - Promise.all(adminProjects.map(p => p._loadTokens())) + Promise.all(pending.map(p => p._reloadTokens())) + .catch(() => {}) .finally(() => { this.projectTokensLoading = false }) }, From 9ad90c7c67a29d54f0dd69bed2ab5621cbf61571 Mon Sep 17 00:00:00 2001 From: Jiachen Fan Date: Thu, 13 Aug 2026 11:28:35 +0800 Subject: [PATCH 5/6] feat(DIARCHERS-1650): add delete action to project tokens on My Tokens page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an Actions column with a delete button to the Project Tokens table, plus a confirmation dialog (mirrors the MCP/Viewer revoke dialogs — shows only the description, no token value). Deletion reuses Project.deleteToken() -> DELETE projects/{id}/tokens/{tid}, the same call used by project settings, which reloads the project's token list on success. Frontend-only; no backend change. --- .../src/components/user/UserGlobalTokens.vue | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/src/dashboard-client/src/components/user/UserGlobalTokens.vue b/src/dashboard-client/src/components/user/UserGlobalTokens.vue index bf1b8268..f20e722b 100644 --- a/src/dashboard-client/src/components/user/UserGlobalTokens.vue +++ b/src/dashboard-client/src/components/user/UserGlobalTokens.vue @@ -278,6 +278,7 @@ Description Read Write + Actions @@ -301,14 +302,20 @@ check close + + + delete + Delete token + + - Loading... + Loading... - No project tokens found. + No project tokens found. @@ -362,6 +369,16 @@ md-cancel-text="Cancel" @close="onMcpRevokeClose"> + + + + @@ -382,6 +399,7 @@ export default { logLoading: false, projectTokensLoading: false, loadedProjectIds: [], + pendingProjectTokenRevoke: null, form: { description: '', expiresDays: 365 @@ -529,6 +547,24 @@ export default { }) }, + confirmProjectTokenRevoke (project, token) { + this.pendingProjectTokenRevoke = { project, token } + this.$refs['projectTokenRevokeDialog'].open() + }, + + onProjectTokenRevokeClose (type) { + if (type !== 'ok' || !this.pendingProjectTokenRevoke) { + this.pendingProjectTokenRevoke = null + return + } + const { project, token } = this.pendingProjectTokenRevoke + // Reuse the same Project.deleteToken() used by project settings — it + // DELETEs projects/{id}/tokens/{tid} and reloads that project's token + // list on success. + project.deleteToken(token.id) + .finally(() => { this.pendingProjectTokenRevoke = null }) + }, + onRevokeClose (type) { if (type !== 'ok' || !this.pendingRevoke) { this.pendingRevoke = null From 3d2ba313a56167a9516feeb173176bb01c5d2fdc Mon Sep 17 00:00:00 2001 From: Jiachen Fan Date: Thu, 13 Aug 2026 14:28:04 +0800 Subject: [PATCH 6/6] fix(DIARCHERS-1650): align Read/Write check icons under their column headers vue-material renders header text in .md-table-head-text (which has padding-left: 24px) but cell content in .md-table-cell-container (no left padding), so the Read/Write check icons sat to the left of their header labels. A plain text-center/text-align on the cell has no effect because the inner container controls layout. Add a scoped deep rule giving the Read/Write cells' inner container the same 24px left padding as the header text, so the icons line up under their column titles. Actions is left unchanged. --- .../src/components/user/UserGlobalTokens.vue | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/dashboard-client/src/components/user/UserGlobalTokens.vue b/src/dashboard-client/src/components/user/UserGlobalTokens.vue index f20e722b..f18c2a00 100644 --- a/src/dashboard-client/src/components/user/UserGlobalTokens.vue +++ b/src/dashboard-client/src/components/user/UserGlobalTokens.vue @@ -294,11 +294,11 @@ {{ token.description }} - + check close - + check close @@ -691,6 +691,14 @@ export default {