diff --git a/src/dashboard-client/src/components/user/UserGlobalTokens.vue b/src/dashboard-client/src/components/user/UserGlobalTokens.vue index ecb34e58..f18c2a00 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) + + + + + + + Project + + {{ p.name }} + + + + Token Description (e.g. "Jenkins Integration") + + + + add_circle + Create project token + + + + You have no projects with admin rights to create tokens for. + + + + + @@ -251,6 +278,7 @@ Description Read Write + Actions @@ -266,22 +294,28 @@ {{ token.description }} - + check close - + check close + + + delete + Delete token + + - Loading... + Loading... - No project tokens found. + No project tokens found. @@ -335,6 +369,16 @@ md-cancel-text="Cancel" @close="onMcpRevokeClose"> + + + + @@ -354,10 +398,16 @@ export default { accessLog: [], logLoading: false, projectTokensLoading: false, + loadedProjectIds: [], + pendingProjectTokenRevoke: null, form: { description: '', expiresDays: 365 }, + projectTokenForm: { + projectId: '', + description: '' + }, mcpTokens: [], newMcpToken: '', pendingMcpRevoke: null, @@ -375,6 +425,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 @@ -406,15 +460,35 @@ 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 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 () { + // 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(pending.map(p => p._reloadTokens())) + .catch(() => {}) + .finally(() => { this.projectTokensLoading = false }) + }, + formatDate (v) { return v ? moment(v).format('YYYY-MM-DD HH:mm:ss') : '-' }, @@ -458,6 +532,39 @@ 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 = '' + }) + }, + + 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 @@ -584,6 +691,14 @@ export default {