11<template >
22 <div class =" tool-permissions" data-el =" expert-tool-permissions" >
33 <section class =" tool-permissions__section" >
4- <span class =" tool-permissions__section-title" >Default permission by action type</span >
5- <ul class =" tool-permissions__rows" >
6- <li v-for =" cls in classDefaults" :key =" cls.key" class =" tool-permissions__row" >
7- <div class =" tool-permissions__label" >
8- <span class =" tool-permissions__label-title" >{{ cls.label }}</span >
9- <span class =" tool-permissions__label-hint" >{{ cls.hint }}</span >
10- </div >
11- <ff-listbox
12- class =" tool-permissions__control"
13- selector =" expert-tool-permission"
14- :model-value =" toolDefaults[cls.key]"
15- :options =" policyOptions"
16- :options-min-width =" 140"
17- @update:model-value =" (value) => setToolClassDefault(cls.key, value)"
18- />
19- </li >
20- </ul >
4+ <FormHeading >Default permission by action type</FormHeading >
5+ <ff-data-table :columns =" defaultColumns" :show-search =" false" >
6+ <template #rows >
7+ <ff-data-table-row v-for =" cls in classDefaults" :key =" cls.key" >
8+ <ff-data-table-cell >
9+ <div class =" tool-permissions__cell" >
10+ <span class =" tool-permissions__title" >{{ cls.label }}</span >
11+ <span class =" tool-permissions__hint" >{{ cls.hint }}</span >
12+ </div >
13+ </ff-data-table-cell >
14+ <ff-data-table-cell class =" permission-col" >
15+ <ff-listbox
16+ class =" tool-permissions__control"
17+ selector =" expert-tool-permission"
18+ :model-value =" toolDefaults[cls.key]"
19+ :options =" policyOptions"
20+ :options-min-width =" 140"
21+ @update:model-value =" (value) => setToolClassDefault(cls.key, value)"
22+ />
23+ </ff-data-table-cell >
24+ </ff-data-table-row >
25+ </template >
26+ </ff-data-table >
2127 </section >
2228
2329 <p v-if =" !canUseWriteTools" class =" tool-permissions__notice" >
2430 Your role is read-only, so the Expert can use read-only tools but cannot run actions that change flows.
2531 </p >
2632
2733 <!-- TODO(platform-tools): once Steve's platform-tool work is merged into the agent,
28- the catalog will also carry platform UI / platform tools. Group `groupedTools`
29- by tool target into sibling sections (Platform UI, Platform Tools, Flow Building
30- Tools), each keeping the read/write/delete scope grouping below. Until then only
31- flow-building tools exist, so a single section is rendered. -->
34+ the catalog will also carry platform UI / platform tools. Add sibling sections
35+ (Platform UI, Platform Tools) alongside this one, each its own table. Until then
36+ only flow-building tools exist, so a single section is rendered. -->
3237 <section class =" tool-permissions__section" >
33- <span class =" tool-permissions__section-title" >Flow Building Tools</span >
34- <template v-if =" toolCatalog .length " >
35- <div v-for =" group in groupedTools" :key =" group.key" class =" tool-permissions__group" >
36- <span class =" tool-permissions__group-title" >{{ group.label }}</span >
37- <ul class =" tool-permissions__rows" >
38- <li v-for =" tool in group.tools" :key =" tool.familyKey" class =" tool-permissions__row" >
39- <div class =" tool-permissions__label" >
40- <span class =" tool-permissions__tool-name" >{{ tool.displayName }}</span >
38+ <FormHeading >Flow Building Tools</FormHeading >
39+ <ff-data-table v-if =" toolCatalog.length" :columns =" toolColumns" :show-search =" false" >
40+ <template #rows >
41+ <ff-data-table-row v-for =" tool in resolvedTools" :key =" tool.familyKey" >
42+ <ff-data-table-cell >
43+ <div class =" tool-permissions__cell" >
44+ <span class =" tool-permissions__title" >{{ tool.displayName }}</span >
4145 <span
4246 v-if =" tool.statusHint"
43- class =" tool-permissions__label-hint "
47+ class =" tool-permissions__hint "
4448 :class =" `is-${tool.status}`"
4549 >{{ tool.statusHint }}</span >
4650 </div >
51+ </ff-data-table-cell >
52+ <ff-data-table-cell >
53+ <span class =" tool-permissions__type" >{{ scopeLabel(tool.toolClass) }}</span >
54+ </ff-data-table-cell >
55+ <ff-data-table-cell class =" permission-col" >
4756 <span
4857 v-if =" tool.control !== 'toggle'"
4958 class =" tool-permissions__static"
5766 :options-min-width =" 140"
5867 @update:model-value =" (value) => setFamilyPreference(tool.variantKeys, value)"
5968 />
60- </li >
61- </ul >
62- </div >
63- </template >
69+ </ff-data-table-cell >
70+ </ff-data-table-row >
71+ </template >
72+ </ff-data-table >
6473 <p v-else class =" tool-permissions__empty" >No flow-building tools available yet.</p >
6574 </section >
6675 </div >
@@ -73,6 +82,7 @@ import SemVer from 'semver'
7382import { hasAMinimumTeamRoleOf } from ' ../../../composables/Permissions.js'
7483import FfListbox from ' ../../../ui-components/components/form/ListBox.vue'
7584import { Roles } from ' ../../../utils/roles.js'
85+ import FormHeading from ' ../../FormHeading.vue'
7686
7787import { useContextStore } from ' @/stores/context.js'
7888import { classOf , useProductAssistantStore } from ' @/stores/product-assistant.js'
@@ -86,7 +96,7 @@ const NR_ASSISTANT = 'nr‑assistant'
8696
8797export default {
8898 name: ' ToolPermissionsSettings' ,
89- components: { FfListbox },
99+ components: { FfListbox, FormHeading },
90100 computed: {
91101 ... mapState (useProductAssistantStore, [' toolCatalog' , ' toolDefaults' , ' toolPolicyFor' , ' toolAvailabilityFor' ]),
92102 ... mapState (useContextStore, [' teamMembership' ]),
@@ -100,17 +110,31 @@ export default {
100110 { label: ' Never' , value: ' deny' }
101111 ]
102112 },
113+ defaultColumns () {
114+ return [
115+ { label: ' Action' , key: ' action' },
116+ { label: ' Permission' , key: ' permission' , class: ' permission-col' }
117+ ]
118+ },
119+ toolColumns () {
120+ return [
121+ { label: ' Tool' , key: ' tool' },
122+ { label: ' Type' , key: ' type' },
123+ { label: ' Permission' , key: ' permission' , class: ' permission-col' }
124+ ]
125+ },
103126 classDefaults () {
104127 return [
105128 { key: ' read' , label: ' Read' , hint: ' View flows, palette and nodes' },
106129 { key: ' write' , label: ' Write' , hint: ' Add or change nodes, wires and tabs' },
107130 { key: ' delete' , label: ' Delete' , hint: ' Remove nodes, tabs and flows' }
108131 ]
109132 },
110- groupedTools () {
133+ resolvedTools () {
111134 // Collapse versioned variants (e.g. "Manage Groups v1/v2") into one family,
112135 // keyed by stripped display name + class, then resolve each family against
113- // the instance's nr-assistant version into a single displayed row.
136+ // the instance's nr-assistant version into a single displayed row. The flat
137+ // list is sorted read -> write -> delete so the table reads scope-first.
114138 const families = new Map ()
115139 for (const entry of this .toolCatalog ) {
116140 const displayName = this .displayName (entry)
@@ -136,17 +160,17 @@ export default {
136160 })
137161 }
138162
139- const buckets = { read: [], write: [], delete: [] }
140- for (const t of resolved) {
141- (buckets[t .toolClass ] || buckets .write ).push (t)
142- }
143- return CLASS_ORDER
144- .filter (key => buckets[key].length )
145- .map (key => ({ key, label: CLASS_LABELS [key], tools: buckets[key] }))
163+ return resolved .sort ((a , b ) =>
164+ CLASS_ORDER .indexOf (a .toolClass ) - CLASS_ORDER .indexOf (b .toolClass ) ||
165+ a .displayName .localeCompare (b .displayName )
166+ )
146167 }
147168 },
148169 methods: {
149170 ... mapActions (useProductAssistantStore, [' setToolPreference' , ' setToolClassDefault' ]),
171+ scopeLabel (cls ) {
172+ return CLASS_LABELS [cls] || CLASS_LABELS .write
173+ },
150174 // Hide the internal version suffix (e.g. "Manage Groups v2") from the
151175 // user-facing label; versioned variants are reconciled into one family.
152176 displayName (tool ) {
@@ -228,99 +252,64 @@ export default {
228252.tool-permissions {
229253 display : flex ;
230254 flex-direction : column ;
231- gap : 1 rem ;
255+ gap : 1.5 rem ;
232256 width : 100% ;
233257}
234258
235259.tool-permissions__section {
236260 display : flex ;
237261 flex-direction : column ;
238- gap : 0.625rem ;
239- }
240-
241- .tool-permissions__section-title {
242- font-size : 0.75rem ;
243- font-weight : 600 ;
244- color : var (--ff-color-text );
245262}
246263
247- .tool-permissions__group {
248- display : flex ;
249- flex-direction : column ;
250- gap : 0.375rem ;
251- }
252-
253- .tool-permissions__group-title {
254- font-size : 0.6875rem ;
255- font-weight : 600 ;
256- text-transform : uppercase ;
257- letter-spacing : 0.04em ;
258- color : var (--ff-color-text-subtle );
259- }
260-
261- .tool-permissions__rows {
262- list-style : none ;
263- margin : 0 ;
264- padding : 0 ;
265- display : flex ;
266- flex-direction : column ;
267- gap : 0.5rem ;
268- }
269-
270- .tool-permissions__row {
271- display : flex ;
272- align-items : center ;
273- justify-content : space-between ;
274- gap : 0.75rem ;
275- }
276-
277- .tool-permissions__label {
264+ .tool-permissions__cell {
278265 display : flex ;
279266 flex-direction : column ;
280267 gap : 1px ;
281- min-width : 0 ;
282268}
283269
284- .tool-permissions__label-title {
285- font-size : 0.8125rem ;
270+ .tool-permissions__title {
286271 font-weight : 500 ;
287272 color : var (--ff-color-text );
288273}
289274
290- .tool-permissions__label-hint {
291- font-size : 0.6875 rem ;
275+ .tool-permissions__hint {
276+ font-size : 0.75 rem ;
292277 color : var (--ff-color-text-subtle );
293278
294279 & .is-requires-update { color : var (--ff-color-status-info-text ); }
295280 & .is-deprecated { color : var (--ff-color-status-error-text ); }
296281}
297282
298- .tool-permissions__tool-name {
283+ .tool-permissions__type {
284+ color : var (--ff-color-text-subtle );
285+ }
286+
287+ .tool-permissions__static {
299288 font-size : 0.8125rem ;
300- color : var (--ff-color-text-strong );
301- overflow-wrap : anywhere;
289+ color : var (--ff-color-text-subtle );
302290}
303291
304292.tool-permissions__notice ,
305293.tool-permissions__empty {
306294 margin : 0 ;
307- font-size : 0.75 rem ;
295+ font-size : 0.8125 rem ;
308296 color : var (--ff-color-text-subtle );
309297}
310298
311- .tool-permissions__static {
312- flex : none ;
313- font-size : 0.75rem ;
314- color : var (--ff-color-text-subtle );
299+ // Keep the permission column hugging the right edge so each tool name lines up
300+ // with its own control across the row border, rather than floating in whitespace.
301+ :deep(.permission-col ) {
302+ width : 1px ;
303+ white-space : nowrap ;
304+ text-align : right ;
315305}
316306
317307// Compact the shared listbox so the control column stays narrow and the menu
318308// never sizes itself to the widest option label. The button renders in-place
319309// (only the options popup is teleported), so :deep reaches it from this scope.
320310:deep(.tool-permissions__control ) {
321- flex : none ;
322- width : 8.5rem ;
323311 min-width : 0 ;
312+ width : 9rem ;
324313
325314 .ff-button {
326315 padding : 0.25rem 0.5rem ;
0 commit comments