Skip to content

Commit 8a97b8e

Browse files
author
andypalmi
committed
refactor(expert): align tool-permissions UI with FlowFuse patterns, drop descriptions
Use FormHeading for the section titles and ff-data-table for both the action-type defaults and the flow-building tool list, replacing the bespoke section/group styling and the non-standard uppercase scope headers. Bordered table rows pair each tool with its permission control across the row rather than leaving them to float across whitespace; tool scope moves into a Type column. Stop surfacing tool descriptions for now: the approval card no longer sends or renders a summary. Raw tool descriptions are written for the model and need a cleanup pass before they read well to a builder; friendly name, scope and call parameters are enough to decide on. The summary can be re-introduced once descriptions are cleaned up.
1 parent a546212 commit 8a97b8e

4 files changed

Lines changed: 87 additions & 109 deletions

File tree

frontend/src/components/expert/components/ToolPermissionsSettings.vue

Lines changed: 87 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,58 @@
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"
@@ -57,10 +66,10 @@
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'
7382
import { hasAMinimumTeamRoleOf } from '../../../composables/Permissions.js'
7483
import FfListbox from '../../../ui-components/components/form/ListBox.vue'
7584
import { Roles } from '../../../utils/roles.js'
85+
import FormHeading from '../../FormHeading.vue'
7686
7787
import { useContextStore } from '@/stores/context.js'
7888
import { classOf, useProductAssistantStore } from '@/stores/product-assistant.js'
@@ -86,7 +96,7 @@ const NR_ASSISTANT = 'nr‑assistant'
8696
8797
export 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: 1rem;
255+
gap: 1.5rem;
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.6875rem;
275+
.tool-permissions__hint {
276+
font-size: 0.75rem;
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.75rem;
295+
font-size: 0.8125rem;
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;

frontend/src/components/expert/components/messages/components/AnswerWrapper.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@
9696
<tool-approval-card
9797
v-if="shouldShowToolApproval"
9898
:name="answer.name"
99-
:summary="answer.summary"
10099
:tool-class="answer.toolClass"
101100
:params="answer.params"
102101
:status="answer.status"

frontend/src/components/expert/components/messages/components/resources/ToolApprovalCard.vue

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
{{ classLabel }}
77
</span>
88
</div>
9-
<p v-if="summary" class="tool-approval-summary">{{ summary }}</p>
109
<dl v-if="paramEntries.length" class="tool-approval-params">
1110
<template v-for="entry in paramEntries" :key="entry.key">
1211
<dt>{{ entry.key }}</dt>
@@ -45,10 +44,6 @@ export default {
4544
type: String,
4645
default: 'this tool'
4746
},
48-
summary: {
49-
type: String,
50-
default: ''
51-
},
5247
toolClass: {
5348
type: String,
5449
default: ''
@@ -118,10 +113,6 @@ export default {
118113
&.tag-delete { background: var(--ff-color-status-error-bg); color: var(--ff-color-status-error-text); }
119114
}
120115
121-
.tool-approval-summary {
122-
margin: 0;
123-
}
124-
125116
.tool-approval-params {
126117
margin: 0;
127118
display: grid;

frontend/src/stores/product-expert.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,6 @@ export const useProductExpertStore = defineStore('product-expert', {
471471
id,
472472
toolKey: payload.tool,
473473
name: payload.name,
474-
summary: payload.summary,
475474
toolClass: payload.toolClass,
476475
params: payload.params,
477476
status: 'pending'

0 commit comments

Comments
 (0)