diff --git a/docs/context/interface/dashboard.md b/docs/context/interface/dashboard.md index 0a165b78..1b77e56d 100644 --- a/docs/context/interface/dashboard.md +++ b/docs/context/interface/dashboard.md @@ -704,8 +704,11 @@ inline-confirms. The nav's **Connections** entry carries the count of cards need as a secret named for the provider (`namedKeys`: `treg secret add apollo …`, or a Secrets row) is a row of `connAccounts` too, on Connections and on its provider's page, and counts in `connCount` — the credential ladder treats it as that provider's key — marked *Saved*, or *Not in use* when a connected -credential for the same provider outranks it, with **Verify and connect** to run it through the -connect probe. **Add a connection** is every provider this server can connect +credential for the same provider outranks it (pasting a key through the provider's button does +exactly that). `loadConnections` fetches `/secrets` in the same `Promise.all` as `/connections`, because +a named key is told from a connection by comparing the two: one list older than the other showed a +removed key, or the previous team's, as saved. A grant with no catalog provider (an own-app OAuth +connect) has no card; it stays on Secrets. A failed Remove reports in the page's banner (`secretErr`). **Add a connection** is every provider this server can connect (`connectable`: `configured` ones only — a provider it holds no client credentials for could only show a dead button; an account already connected to one still shows above), in `providerGroups` (grouped by the registry's category, filtered by `connQ` and by `connKind` — the **All · Your @@ -755,7 +758,7 @@ flag = whether *this* deployment can run at least one connect flow) and **`GET / (`list_connections` — the org's existing grants). Each authorization method also has its own `configured` flag. For a multi-method provider, the registry sets the provider flag when any one method is available, so a configured secondary grant cannot be hidden by an unavailable primary grant. -`go('connections')` also loads **`GET /secrets`**, which `namedKeys` reads. +`loadConnections` also reads **`GET /secrets`** for a signed-in member, which `namedKeys` reads. **Consent disclosure.** A provider row may carry a **`consent_notice`**, rendered as a `.mk-notice` panel in two places: under the Connect button on the integration page and inside the `capAsk` modal, diff --git a/frontend/src/components/ConnectionCard.vue b/frontend/src/components/ConnectionCard.vue index 6f6c71d7..73fdc732 100644 --- a/frontend/src/components/ConnectionCard.vue +++ b/frontend/src/components/ConnectionCard.vue @@ -48,11 +48,10 @@ export default { · added by {{short(owner)}}
- +{{mkProvider.consent_notice}}
- + diff --git a/frontend/src/state/connections.js b/frontend/src/state/connections.js index f7e11887..84b19aff 100644 --- a/frontend/src/state/connections.js +++ b/frontend/src/state/connections.js @@ -6,12 +6,18 @@ export default { this.loadPlatforms(); // fire-and-forget, and first: the catalog must neither hold up nor wait for the connect UI try{ // The provider list is the deployment's, fixed for the session: fetched once, not per view. - const [ps, cs]=await Promise.all([ + // Secrets come in the same breath: a key saved under a provider's name is told apart from a + // connection by comparing the two lists, so one fetched later than the other (a team switch, + // a disconnect, a slow first answer) would show a removed or foreign key as saved. + const member=this.authed && !this.publicCatalog; + const [ps, cs, ss]=await Promise.all([ this.providers.length ? this.providers : fetch('/oauth/providers').then(r=>r.json()).catch(()=>[]), this.api('/connections').catch(()=>[]), + member ? this.api('/secrets').catch(()=>null) : null, ]); if(!live()) return; this.providers=ps||[]; this.connections=cs||[]; + if(ss) this.secrets=ss; }catch(e){ if(live()) this.connErr=String(e.message||e); } }, authorizationMethodSpec(providerName, methodName){ diff --git a/frontend/src/state/connectionsComputed.js b/frontend/src/state/connectionsComputed.js index 02384aaa..5080df3c 100644 --- a/frontend/src/state/connectionsComputed.js +++ b/frontend/src/state/connectionsComputed.js @@ -17,16 +17,18 @@ providerIndex(){ return new Map(this.providers.map(p=>[p.service,p])); }, }, // What the Secrets page lists: the credentials the team's own tools use, and nothing Connections shows. ownSecrets(){ - const shown=new Set([...this.connections.map(c=>c.id), ...this.namedKeys.map(k=>k.s.id)]); + const shown=new Set(this.connAccounts.map(a=>(a.c||a.s).id)); return this.secrets.filter(s=>!shown.has(s.id)); }, // Every credential the team holds for a catalog provider, as one kind of row whichever way it was // added: a connection (`c`) or a named key (`s`). `pasted` is computed once here because every // card asks it several times. connAccounts(){ - const conns=this.connections.map(c=>{ const p=this.providerIndex.get(c.provider)||null; - return {id:'c'+c.id, service:c.provider, c, p, pasted:this.pastedCredential(p), - name:(p&&p.display_name)||c.provider||c.name, st:this.connState(c)}; }); + // A grant with no catalog provider (an own-app OAuth connect) has no provider page to manage it + // from: it stays among the team's own secrets. + const conns=this.connections.flatMap(c=>{ const p=this.providerIndex.get(c.provider); + return p ? [{id:'c'+c.id, service:c.provider, c, p, pasted:this.pastedCredential(p), + name:p.display_name, st:this.connState(c)}] : []; }); const named=this.namedKeys.map(({s, p, shadowed})=>({id:'s'+s.id, service:p.service, s, p, pasted:true, name:p.display_name, st:shadowed ? {key:'ok', tone:'quiet', label:'Not in use', title:'The connected '+p.display_name+' credential is used instead'} diff --git a/frontend/src/state/navigation.js b/frontend/src/state/navigation.js index 0ad5e6d1..5a80891d 100644 --- a/frontend/src/state/navigation.js +++ b/frontend/src/state/navigation.js @@ -11,7 +11,7 @@ go(v, fromPop){ this.resetConfirms(); this.mobileNav=false; this.drawerTool=null // working so an existing /app#usage link, and the balance card's deep link, still land right. if(v==='usage'){ this.actTab='usage'; v='activity'; this.loadUsage(); } else if(v==='activity'){ this.actTab='feed'; } - this.detail=null; this.view=v; if(v==='activity')this.loadCalls(); if(v==='admin')this.loadAdmin(); if(v==='orgs'){this.loadOrgAdmin(); this.loadMyUsage(); this.loadBilling();} if(v==='usage')this.loadUsage(); if(v==='secrets'){this.loadSecrets(); if(!this.providers.length)this.loadConnections();} if(v==='resources')this.loadTeamResources(); if(v==='catalog')this.loadConnections(); if(v==='connections'){this.loadConnections(); this.loadSecrets();} if(v==='referrals')this.loadReferrals(); if(v==='hub')this.loadHub(); + this.detail=null; this.view=v; if(v==='activity')this.loadCalls(); if(v==='admin')this.loadAdmin(); if(v==='orgs'){this.loadOrgAdmin(); this.loadMyUsage(); this.loadBilling();} if(v==='usage')this.loadUsage(); if(v==='secrets'){this.loadSecrets(); if(!this.providers.length)this.loadConnections();} if(v==='resources')this.loadTeamResources(); if(v==='catalog')this.loadConnections(); if(v==='connections')this.loadConnections(); if(v==='referrals')this.loadReferrals(); if(v==='hub')this.loadHub(); // push history so browser Back navigates BETWEEN views instead of leaving the app; the '/app' // pathname also walks back from a /app/skills/