@revenexx/studio-integrations 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/README.md +17 -4
  3. package/dist/module.json +1 -1
  4. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +15 -14
  5. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +91 -19
  6. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -21
  7. package/dist/runtime/components/Integrations/CredentialTypePicker.vue +4 -8
  8. package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
  9. package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -2
  10. package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +1 -0
  11. package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
  12. package/dist/runtime/components/Integrations/ExpressionInput.vue +54 -25
  13. package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
  14. package/dist/runtime/components/Integrations/GroupFrameView.vue +12 -12
  15. package/dist/runtime/components/Integrations/Layout.vue +21 -2
  16. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +10 -11
  17. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +2 -5
  18. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
  19. package/dist/runtime/components/Integrations/NodeConfigField.vue +160 -67
  20. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +2 -0
  21. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +6 -0
  22. package/dist/runtime/components/Integrations/NodeInspector.vue +1075 -303
  23. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +6 -0
  24. package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +12 -0
  25. package/dist/runtime/components/Integrations/NodeLogo.vue +2 -30
  26. package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +12 -0
  27. package/dist/runtime/components/Integrations/NodeMark.d.vue.ts +15 -0
  28. package/dist/runtime/components/Integrations/NodeMark.vue +79 -0
  29. package/dist/runtime/components/Integrations/NodeMark.vue.d.ts +15 -0
  30. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +21 -0
  31. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +42 -0
  32. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
  33. package/dist/runtime/components/Integrations/NodePalette.vue +100 -34
  34. package/dist/runtime/components/Integrations/QueryBuilder.vue +8 -1
  35. package/dist/runtime/components/Integrations/RunActionsMenu.vue +3 -1
  36. package/dist/runtime/components/Integrations/RunPayloadInput.vue +17 -4
  37. package/dist/runtime/components/Integrations/StickyNoteView.vue +2 -1
  38. package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
  39. package/dist/runtime/components/Integrations/TemplateCard.vue +109 -0
  40. package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +18 -0
  41. package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +2 -2
  42. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +7 -10
  43. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
  44. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +21 -37
  45. package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
  46. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +23 -7
  47. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
  48. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +22 -1
  49. package/dist/runtime/components/Integrations/WorkflowEditor.vue +170 -48
  50. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +22 -1
  51. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +4 -20
  52. package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
  53. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +11 -2
  54. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +19 -1
  55. package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +4 -0
  56. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +189 -46
  57. package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
  58. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +4 -20
  59. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +15 -9
  60. package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
  61. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +7 -2
  62. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
  63. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +4 -2
  64. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +30 -17
  65. package/dist/runtime/components/Integrations/WorkflowsTable.vue +47 -7
  66. package/dist/runtime/composables/useCredentialTypes.js +4 -4
  67. package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
  68. package/dist/runtime/composables/useIntegrationsCredentials.js +4 -4
  69. package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
  70. package/dist/runtime/composables/useIntegrationsNodes.js +4 -4
  71. package/dist/runtime/composables/useIntegrationsRuns.js +11 -6
  72. package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
  73. package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
  74. package/dist/runtime/composables/useIntegrationsSecrets.js +10 -7
  75. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +1 -1
  76. package/dist/runtime/composables/useIntegrationsTemplates.js +12 -8
  77. package/dist/runtime/composables/useIntegrationsTriggers.js +6 -6
  78. package/dist/runtime/composables/useIntegrationsWorkflows.js +8 -4
  79. package/dist/runtime/composables/useLoadGate.d.ts +17 -0
  80. package/dist/runtime/composables/useLoadGate.js +8 -0
  81. package/dist/runtime/data/branchReorder.d.ts +37 -0
  82. package/dist/runtime/data/branchReorder.js +71 -0
  83. package/dist/runtime/data/handleId.d.ts +23 -0
  84. package/dist/runtime/data/handleId.js +6 -0
  85. package/dist/runtime/data/integrationTemplates.d.ts +39 -0
  86. package/dist/runtime/data/integrationTemplates.js +15 -0
  87. package/dist/runtime/data/integrationWorkflowSamples.js +1 -1
  88. package/dist/runtime/data/nodeIcons.d.ts +2 -0
  89. package/dist/runtime/data/nodeIcons.js +73 -0
  90. package/dist/runtime/data/nodeInputData.d.ts +16 -4
  91. package/dist/runtime/data/nodeInputData.js +4 -0
  92. package/dist/runtime/data/payloadForm.d.ts +9 -2
  93. package/dist/runtime/data/payloadForm.js +27 -3
  94. package/dist/runtime/data/portTone.d.ts +17 -0
  95. package/dist/runtime/data/portTone.js +7 -0
  96. package/dist/runtime/data/webhookAuth.d.ts +18 -0
  97. package/dist/runtime/data/webhookAuth.js +19 -0
  98. package/dist/runtime/data/workflowIconCatalog.js +2 -5
  99. package/dist/runtime/pages/integrations/audit.vue +91 -6
  100. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +6 -4
  101. package/dist/runtime/pages/integrations/credentials.vue +119 -21
  102. package/dist/runtime/pages/integrations/index.vue +49 -44
  103. package/dist/runtime/pages/integrations/runs.vue +1 -1
  104. package/dist/runtime/pages/integrations/schedules.vue +51 -7
  105. package/dist/runtime/pages/integrations/secrets.vue +29 -7
  106. package/dist/runtime/pages/integrations/templates/[slug].vue +52 -30
  107. package/dist/runtime/pages/integrations/templates/index.vue +57 -86
  108. package/dist/runtime/pages/integrations/workflows/[id].vue +243 -25
  109. package/dist/runtime/pages/integrations/workflows/index.vue +15 -2
  110. package/dist/runtime/types/integrations.d.ts +65 -6
  111. package/dist/runtime/utils/apiError.d.ts +47 -0
  112. package/dist/runtime/utils/apiError.js +32 -0
  113. package/dist/runtime/utils/cockpitSources.d.ts +153 -0
  114. package/dist/runtime/utils/cockpitSources.js +14 -0
  115. package/dist/runtime/utils/loadGate.d.ts +61 -0
  116. package/dist/runtime/utils/loadGate.js +22 -0
  117. package/dist/runtime/utils/localize.d.ts +29 -0
  118. package/dist/runtime/utils/localize.js +4 -0
  119. package/dist/runtime/utils/notify.d.ts +59 -0
  120. package/dist/runtime/utils/notify.js +21 -0
  121. package/dist/runtime/utils/revenexxCredentialSource.d.ts +26 -0
  122. package/dist/runtime/utils/revenexxCredentialSource.js +8 -0
  123. package/dist/runtime/utils/schedule.d.ts +141 -0
  124. package/dist/runtime/utils/schedule.js +207 -0
  125. package/dist/runtime/utils/validationFindings.d.ts +101 -0
  126. package/dist/runtime/utils/validationFindings.js +53 -0
  127. package/dist/runtime/utils/whenIdle.d.ts +42 -0
  128. package/dist/runtime/utils/whenIdle.js +12 -0
  129. package/package.json +8 -2
package/CHANGELOG.md ADDED
@@ -0,0 +1,201 @@
1
+ # @revenexx/studio-integrations
2
+
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3686d9e: The node inspector's controls report honest states — a field that is loading,
8
+ empty or unavailable says which, instead of all three looking alike — and the
9
+ trigger surface gains a payload form built from the trigger's own shape rather
10
+ than a free-text box. Rows keep stable keys, so editing one no longer re-orders
11
+ the rest.
12
+ - 3686d9e: Activating a workflow the platform refuses now says why. The validation feedback
13
+ the service returns is surfaced in the editor instead of the activation failing
14
+ silently, and a refusal that carries no readable detail falls back to wording
15
+ that still tells the user what happened.
16
+ - 3686d9e: Placing a node no longer forces its configuration open. Dropping a node onto the
17
+ canvas just places it; you open the settings when you want them, through an edit
18
+ affordance that now stays visible on the node card rather than appearing only on
19
+ hover. The affordance is shown only for nodes that actually have something to
20
+ configure.
21
+ - 3686d9e: A node that ships in several versions lets you choose which one you place,
22
+ instead of the palette silently picking one. The chosen version is shown on the
23
+ node card, so a workflow built against an older version is readable on the
24
+ canvas.
25
+ - 3686d9e: A node is now marked by its own symbol combined with a vendor badge, so two
26
+ nodes from the same vendor stay distinguishable while still reading as that
27
+ vendor's. The badge is drawn only where it adds something — including search
28
+ hits in the palette — and a node with no symbol of its own is marked honestly
29
+ rather than given a stand-in.
30
+
31
+ The component behind this is renamed `NodeLogo` → `NodeMark`. `NodeLogo` remains
32
+ as a shim so hosts that reference it keep working.
33
+
34
+ - 3686d9e: The canvas says which output path leads the eye. Edges now carry importance
35
+ rather than colour, so the main path reads as the main path, and colour is
36
+ reserved for the two states where it means something — green for the ordinary
37
+ output, rose for the error output. The inspector's port dots use the same neutral
38
+ tone as the canvas, and an error port's label stays legible instead of being
39
+ tinted into the background.
40
+ - 3686d9e: The mouse wheel zooms the canvas instead of panning it vertically — on the
41
+ editor and on the read-only canvases alike. The zoom readout announces its
42
+ percentage again, so the level is available to screen readers and not only to
43
+ the eye.
44
+ - 3686d9e: Node cards drop the generated subtitle and surface the node's note instead. The
45
+ note is marked in the card's corner and its panel flips open from there, so a
46
+ card carries what somebody wrote about the node rather than a line derived from
47
+ its configuration. The marker is reachable from the keyboard, and the note's
48
+ description stays in the workflow tree rather than living only in the panel.
49
+ - 3686d9e: A node can say what it is for. The description from the node's manifest is shown
50
+ in the inspector, and the note moves out of the settings into its own place, so
51
+ the text the vendor wrote and the text you wrote are no longer mixed into one
52
+ field. The explanation can be reached by pointer as well as keyboard, and Escape
53
+ closes the note without closing anything behind it. Whitespace alone no longer
54
+ counts as a note.
55
+ - 3686d9e: A workflow variable has a value, not a default. The field is named for what it
56
+ actually is — the value the workflow runs with — which removes the suggestion
57
+ that something else would override it, and a variable can be marked as required
58
+ so an incomplete workflow is caught before it runs.
59
+ - 3686d9e: Configuring a node is simpler. The configuration itself gets the dialog, with
60
+ mapping and testing available on call rather than competing for the same space,
61
+ and the separate outputs strip is gone — the third tab is **Output**. A setting
62
+ the last save refused is marked as such, so a rejected save says which field it
63
+ was about, and the dialog is sized to its content instead of leaving the fold to
64
+ hide a control. Switching away from a tab the bar no longer draws leaves nothing
65
+ selected behind.
66
+ - 3686d9e: You can say when a workflow runs without writing cron. The schedules page offers
67
+ the choice directly, a schedule can fire several times a day, and every schedule
68
+ states when it next fires instead of showing an expression and leaving you to read
69
+ it.
70
+
71
+ The reading is exact rather than approximate: a step that does not divide its
72
+ field is no longer described as a plain interval, and a named weekday no longer
73
+ widens a schedule to the whole week.
74
+
75
+ - 3686d9e: A webhook trigger can say who is allowed to call it. Protection profiles are
76
+ offered on the trigger, the secret behind one can be rotated, and exposing an
77
+ endpoint persists the default profile rather than leaving the webhook open until
78
+ somebody sets one.
79
+
80
+ ### Patch Changes
81
+
82
+ - 3686d9e: The node inspector's credential picker separates the credential's name from its
83
+ state, and reports the state it was actually told rather than deriving one. A
84
+ deleted credential no longer reads as though none had been chosen — the dead
85
+ reference is announced, not only drawn — and a failed test keeps a readable
86
+ reason.
87
+ - 3686d9e: **Tidy up** no longer leaves a node's outputs crossing each other. After an
88
+ auto-layout the out and error branches keep to their own lanes, and where one
89
+ output fans out to several nodes the edge bends are staggered — mirrored on both
90
+ sides — so sibling branches read alike instead of overlapping into a single
91
+ line.
92
+ - 3686d9e: The hover toolbar no longer appears on canvases you cannot edit. On a run replay
93
+ or any other read-only canvas, **Duplicate** and **Delete** used to show up on
94
+ hover and do nothing; they are now absent instead of dead.
95
+ - 3686d9e: **Tidy up** no longer routes a node's ordinary output and its error output across
96
+ each other. Branches are re-slotted so the two leave the spine without crossing,
97
+ and where the current order gives no answer the target node's id breaks the tie —
98
+ so the same workflow tidies up the same way every time.
99
+ - 3686d9e: A node that ends the run no longer draws an output port. Stop and error nodes
100
+ declare no outputs, so the canvas gave them a port that could never lead
101
+ anywhere; that port is gone, and nothing can be connected to it by accident.
102
+ - 3686d9e: The actions button on a workflow row has an accessible name. It is icon-only and
103
+ sits in a row that offers nothing else to address it by, so it now names the
104
+ workflow it acts on — `Actions for <workflow>` — instead of being unreachable to
105
+ anyone not using a pointer.
106
+ - 3686d9e: Every form field and icon-only control in the studio has an accessible name, and
107
+ each page declares its main content landmark. The skip link now moves focus
108
+ rather than only changing the address, so it actually skips something, and the
109
+ icon picker keeps the name of the popover it opens from.
110
+ - 3686d9e: **Delete** and **Backspace** no longer delete nodes while a dialog or panel
111
+ covers the canvas. The canvas stands its keyboard handling down for as long as an
112
+ overlay is open, so a keystroke meant for the text field in front of you cannot
113
+ remove a node behind it.
114
+ - 3686d9e: The validation banner is summarised per node, and the nodes it names are marked
115
+ on the canvas — so a workflow that will not activate points at where the problem
116
+ is instead of listing findings you have to match up yourself. A finding the
117
+ platform attributes to no node carries only its message rather than being pinned
118
+ to an arbitrary one. Marks are held back while a node is being dragged, so the
119
+ re-sync no longer interrupts the drag.
120
+ - 3686d9e: Running an incomplete workflow names the settings that are missing, the way the
121
+ activation gate already did — instead of a refusal you have to go looking for the
122
+ cause of. A finding the platform attributes to no node is named rather than only
123
+ counted, and a refusal that arrives with nothing readable in it no longer leaves
124
+ the user with no message at all.
125
+ - 3686d9e: A credential's test result is no longer wiped by the reload that follows it. The
126
+ result you just produced stays on screen instead of disappearing the moment the
127
+ list refreshes, and a prototype key is no longer offered as though it were a
128
+ credential type.
129
+ - 3686d9e: The template gallery says which kind of nothing it is showing — no templates at
130
+ all reads differently from nothing matching your search. A template's vendor can
131
+ be searched for, and three names that the detail page relied on but never put on
132
+ screen are now there, so what a template arrives wired with is readable before
133
+ you install it.
134
+ - 3686d9e: The runs and schedules pages stop reporting figures they did not have. Missed
135
+ runs were collected before the workflows they belong to had arrived, so they came
136
+ out short; they are now collected once the list is in. Leaving the page no longer
137
+ takes the count with it, so coming back shows what ran, what runs next and what
138
+ never got to run rather than an empty summary.
139
+ - 3686d9e: Two controls on the workflows list no longer claim a name derived from data that
140
+ had not loaded, so they are addressable as soon as the page is — including the
141
+ route to the create-workflow dialog, which sits behind two doors and had nothing
142
+ stable to reach it by.
143
+ - 3686d9e: The audit log pages properly and closes properly. A second pager sat behind the
144
+ first and could disagree with it; there is now one. Searching for a person finds
145
+ them through the actor column rather than depending on the resource column
146
+ repeating the same value, and the one way of closing an opened record that never
147
+ worked now does.
148
+ - 3686d9e: Credential dialogs no longer come up empty for a managed type. The managed types
149
+ ask whether the cockpit is actually there instead of assuming it, so the fields
150
+ are rendered when the source can answer and the dialog says what it cannot offer
151
+ when it cannot.
152
+ - 3686d9e: The **Created** and **Updated** columns on the secrets list show something. They
153
+ were always empty; they now carry the dates the service records, a cell with no
154
+ date claims no timestamp rather than rendering an epoch, and the dates stay out
155
+ of the search box so filtering by name is not confused by them.
156
+ - 3686d9e: Text that arrives from the API is localised by one resolver instead of fifteen
157
+ private copies, so a label resolves the same way wherever it is shown and a
158
+ missing translation falls back consistently rather than differently per page.
159
+ - 3686d9e: Pages no longer render empty data while their load is still in flight. The ten
160
+ composables that each guarded their own load now share one guard that waits for
161
+ the load already running instead of handing back an empty result, so a second
162
+ caller is answered once the data is there. The workflows list stops claiming
163
+ "Never run" when the runs simply had not arrived yet.
164
+ - 3686d9e: The dashboard's figures are there on the first visit rather than after a reload.
165
+ **Needs attention** reports the state now instead of every failure the workflow
166
+ has ever had, each figure that answers a click is a link and says its number, and
167
+ a request that is counted is recounted when it changes rather than going quiet.
168
+ - 3686d9e: A failed run keeps the reason the platform recorded, and shows the moment the
169
+ failure happened rather than the moment the run started. Where the platform left
170
+ the reason empty the run says so, instead of presenting the blank as though
171
+ nothing had gone wrong.
172
+ - 3686d9e: The dashboard's templates panel is named for what it offers rather than for a
173
+ ranking it never measured. It was headed "Popular templates" while showing no
174
+ measure of popularity; it now reads **Start from a template**.
175
+ - 3686d9e: A connection can only leave an output and arrive at an input. Two input ports
176
+ could be joined to each other and the result saved, producing a workflow the
177
+ platform would never run; that is refused now. Connections already saved the wrong
178
+ way round are not repaired by this change — they have to be redrawn.
179
+ - 3686d9e: Template categories are named from one source, so the filter and the cards cannot
180
+ disagree, and the category list names the two categories that actually ship rather
181
+ than one nobody could select. A template is presented by a single card component in
182
+ two densities instead of two implementations that had drifted apart, and what
183
+ choosing a card does is defined once.
184
+ - 3686d9e: An audit entry opened from a row holds the keyboard and gives it back when it
185
+ closes. The panel used to leave focus in the row behind it, so the keyboard was
186
+ somewhere the user could no longer see.
187
+ - 3686d9e: The template gallery says it is loading in words, not only as a shimmer, so the
188
+ wait is announced rather than being visible only to the eye — and a loading
189
+ gallery no longer looks exactly like an empty one. The announcement and the
190
+ placeholders are derived from the same condition, and the wait ends in words too.
191
+ - 3686d9e: A port name comes back whole. A name containing `__` was cut at the separator, and
192
+ a handle id with no port name in it produced an empty string that read as a real
193
+ name; the first round-trips intact now and the second answers with nothing at all.
194
+ - 3686d9e: The webhook and event trigger copy spells it **catalogue**, matching the rest of
195
+ the studio's British English — in the loading message, the payload hint and the
196
+ line shown when a topic has no sample payload.
197
+ - 3686d9e: Saving a credential says what happened. A save that is refused states its reason
198
+ — in the dialog and at the field it concerns, where a scrolling form cannot hide
199
+ it — rather than the service's count of the other fields, and it reaches every
200
+ field it names. A save that lands closes the dialog. A refused edit says why too,
201
+ instead of failing silently.
package/README.md CHANGED
@@ -60,6 +60,7 @@ cockpit monorepo under `packages/studio-integrations`).
60
60
  pnpm install # runs the module `prepare` (nuxt-module-build)
61
61
  pnpm test # vitest
62
62
  pnpm build # dist/
63
+ pnpm changeset # record what your change does to the version
63
64
  ```
64
65
 
65
66
  Hosts to develop against:
@@ -77,7 +78,19 @@ Hosts to develop against:
77
78
 
78
79
  ## Release
79
80
 
80
- Bump `version` in `package.json` and push to `main` — CI
81
- (`.github/workflows/publish.yml`) publishes any version that isn't on npm
82
- yet, via npm Trusted Publishing (the trusted publisher on npmjs is bound to
83
- this repo + workflow file).
81
+ Versioning is driven by [Changesets](https://github.com/changesets/changesets),
82
+ so `version` in `package.json` is never edited by hand. Every pull request
83
+ records what it does to the version — CI fails without it:
84
+
85
+ ```bash
86
+ pnpm changeset # pick patch/minor/major, then write the note
87
+ pnpm changeset --empty # …for a PR that intentionally releases nothing
88
+ ```
89
+
90
+ Releasing is then merging a pull request. Pushes to `main` that carry changesets
91
+ make CI open a **"Version Packages"** PR with the bump and `CHANGELOG.md`
92
+ applied; merging that PR publishes to npm (Trusted Publishing over OIDC, no
93
+ token) and notifies the cockpit.
94
+
95
+ Full policy, the SemVer contract, and why publishing is deliberately not
96
+ `changeset publish`: [`docs/versioning.md`](docs/versioning.md).
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@revenexx/studio-integrations",
3
3
  "configKey": "studioIntegrations",
4
- "version": "0.5.0",
4
+ "version": "0.6.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
@@ -19,6 +19,7 @@ import {
19
19
  SelectValue
20
20
  } from "@revenexx/studio";
21
21
  import { toast } from "vue-sonner";
22
+ import { resolveLocalized } from "../../utils/localize";
22
23
  const props = defineProps({
23
24
  open: { type: Boolean, required: true },
24
25
  credentialType: { type: [String, Array], required: false }
@@ -26,12 +27,6 @@ const props = defineProps({
26
27
  const emit = defineEmits(["update:open", "created"]);
27
28
  const { create, testInline, load, oauthAuthorizeUrl } = useIntegrationsCredentials();
28
29
  const { load: loadTypes, bySlug } = useCredentialTypes();
29
- function loc(v) {
30
- if (!v) return "";
31
- if (typeof v === "string") return v;
32
- const o = v;
33
- return o.en ?? o.de ?? Object.values(o)[0] ?? "";
34
- }
35
30
  function authKindLabel(k) {
36
31
  switch (k) {
37
32
  case "api-key":
@@ -75,7 +70,7 @@ watch(() => props.open, async (o) => {
75
70
  await loadTypes();
76
71
  if (allowedTypes.value.length === 1) {
77
72
  pickedType.value = allowedTypes.value[0];
78
- form.name = loc(pickedType.value.name);
73
+ form.name = resolveLocalized(pickedType.value.name);
79
74
  } else {
80
75
  pickedType.value = null;
81
76
  form.name = "";
@@ -86,7 +81,7 @@ function pickType(t) {
86
81
  form.config = {};
87
82
  testResult.value = null;
88
83
  sourceActive.value = false;
89
- if (!form.name) form.name = loc(t.name);
84
+ if (!form.name) form.name = resolveLocalized(t.name);
90
85
  }
91
86
  function buildConfigPayload() {
92
87
  const out = {};
@@ -173,7 +168,7 @@ async function submit() {
173
168
  >
174
169
  <IntegrationsCredentialLogo :type="t" :size="18" />
175
170
  <div class="min-w-0 truncate text-sm">
176
- <span class="font-semibold">{{ loc(t.name) }}</span>
171
+ <span class="font-semibold">{{ resolveLocalized(t.name) }}</span>
177
172
  <span class="ml-1.5 text-muted-foreground">({{ authKindLabel(t.auth_kind) }})</span>
178
173
  </div>
179
174
  </button>
@@ -183,7 +178,7 @@ async function submit() {
183
178
  <div v-else class="space-y-3 py-1 max-h-[26rem] overflow-y-auto pr-1">
184
179
  <div class="flex items-center gap-2">
185
180
  <IntegrationsCredentialLogo :type="pickedType" :size="18" />
186
- <div class="text-sm font-medium">{{ loc(pickedType.name) }}</div>
181
+ <div class="text-sm font-medium">{{ resolveLocalized(pickedType.name) }}</div>
187
182
  <button
188
183
  v-if="allowedTypes.length > 1"
189
184
  type="button"
@@ -206,10 +201,14 @@ async function submit() {
206
201
  @apply="(c) => Object.assign(form.config, c)"
207
202
  />
208
203
 
204
+ <!-- Each label names its own control (`for`/`id`): without it a screen
205
+ reader reads the field as unlabelled, and the label text cannot be
206
+ used to reach it at all. -->
209
207
  <div v-for="f in fieldsOf(pickedType)" v-show="!sourceActive" :key="f.key" class="space-y-1.5">
210
- <Label>{{ loc(f.label) }}{{ f.required ? " *" : "" }}</Label>
208
+ <Label :for="`create-field-${f.key}`">{{ resolveLocalized(f.label) }}{{ f.required ? " *" : "" }}</Label>
211
209
  <Input
212
210
  v-if="f.type === 'secret'"
211
+ :id="`create-field-${f.key}`"
213
212
  :model-value="form.config[f.key] ?? ''"
214
213
  type="password"
215
214
  autocomplete="new-password"
@@ -218,30 +217,32 @@ async function submit() {
218
217
  />
219
218
  <Input
220
219
  v-else-if="f.type === 'number'"
220
+ :id="`create-field-${f.key}`"
221
221
  type="number"
222
222
  :model-value="form.config[f.key] ?? ''"
223
223
  class="bg-white"
224
224
  @update:model-value="(v) => form.config[f.key] = v === '' ? void 0 : Number(v)"
225
225
  />
226
226
  <div v-else-if="f.type === 'boolean'" class="flex items-center gap-2 pt-1">
227
- <Switch :model-value="Boolean(form.config[f.key])" @update:model-value="(v) => form.config[f.key] = v" />
227
+ <Switch :id="`create-field-${f.key}`" :model-value="Boolean(form.config[f.key])" @update:model-value="(v) => form.config[f.key] = v" />
228
228
  </div>
229
229
  <Select
230
230
  v-else-if="f.type === 'select'"
231
231
  :model-value="form.config[f.key] ?? ''"
232
232
  @update:model-value="(v) => form.config[f.key] = v"
233
233
  >
234
- <SelectTrigger class="bg-white">
234
+ <SelectTrigger :id="`create-field-${f.key}`" class="bg-white">
235
235
  <SelectValue placeholder="Pick one" />
236
236
  </SelectTrigger>
237
237
  <SelectContent>
238
238
  <SelectItem v-for="opt in f.options ?? []" :key="String(opt.value)" :value="String(opt.value)">
239
- {{ loc(opt.label) }}
239
+ {{ resolveLocalized(opt.label) }}
240
240
  </SelectItem>
241
241
  </SelectContent>
242
242
  </Select>
243
243
  <Input
244
244
  v-else
245
+ :id="`create-field-${f.key}`"
245
246
  :model-value="form.config[f.key] ?? ''"
246
247
  class="bg-white"
247
248
  @update:model-value="(v) => form.config[f.key] = v"
@@ -8,6 +8,12 @@ import {
8
8
  TrashBinTrash as Trash2
9
9
  } from "#solar-icons";
10
10
  import {
11
+ AlertDialog,
12
+ AlertDialogContent,
13
+ AlertDialogDescription,
14
+ AlertDialogFooter,
15
+ AlertDialogHeader,
16
+ AlertDialogTitle,
11
17
  Button,
12
18
  Dialog,
13
19
  DialogContent,
@@ -23,8 +29,11 @@ import {
23
29
  SelectTrigger,
24
30
  SelectValue
25
31
  } from "@revenexx/studio";
26
- import { toast } from "vue-sonner";
32
+ import { toast } from "../../utils/notify";
33
+ import { apiFieldErrors, refusalReason } from "../../utils/apiError";
27
34
  import { credentialState, relativeTime } from "../../utils/credentialState";
35
+ import { hasRevenexxSource } from "../../utils/revenexxCredentialSource";
36
+ import { resolveLocalized } from "../../utils/localize";
28
37
  const props = defineProps({
29
38
  open: { type: Boolean, required: true },
30
39
  credential: { type: [Object, null], required: true },
@@ -43,14 +52,17 @@ const saving = ref(false);
43
52
  const deleting = ref(false);
44
53
  const testing = ref(false);
45
54
  const liveTest = ref(null);
46
- const connecting = ref(false);
47
- function loc(v) {
48
- if (!v) return "";
49
- if (typeof v === "string") return v;
50
- const o = v;
51
- return o.en ?? o.de ?? Object.values(o)[0] ?? "";
55
+ const saveError = ref(null);
56
+ const fieldErrors = ref({});
57
+ function configFieldAria(key) {
58
+ return fieldErrors.value[`config.${key}`] ? { "aria-invalid": "true", "aria-describedby": `detail-field-${key}-error` } : {};
59
+ }
60
+ function clearRefusal() {
61
+ saveError.value = null;
62
+ fieldErrors.value = {};
52
63
  }
53
- const typeName = computed(() => loc(props.type?.name) || props.credential?.credential_type_slug || "");
64
+ const connecting = ref(false);
65
+ const typeName = computed(() => resolveLocalized(props.type?.name) || props.credential?.credential_type_slug || "");
54
66
  const isOAuthAuthcode = computed(() => props.type?.auth_kind === "oauth2-authcode");
55
67
  const state = computed(() => props.credential ? credentialState(props.credential) : null);
56
68
  function fieldsOf(t) {
@@ -65,7 +77,7 @@ function formatDateTime(iso) {
65
77
  }
66
78
  }
67
79
  watch(
68
- () => [props.open, props.credential?.id],
80
+ [() => props.open, () => props.credential?.id],
69
81
  () => {
70
82
  if (!props.open || !props.credential) return;
71
83
  tab.value = "credentials";
@@ -73,6 +85,7 @@ watch(
73
85
  form.config = { ...props.credential.public_config ?? {} };
74
86
  sourceActive.value = false;
75
87
  liveTest.value = null;
88
+ clearRefusal();
76
89
  },
77
90
  { immediate: true }
78
91
  );
@@ -100,20 +113,24 @@ async function runTest() {
100
113
  async function save() {
101
114
  if (!props.credential || !form.name.trim()) return;
102
115
  saving.value = true;
116
+ clearRefusal();
103
117
  try {
104
118
  await update(props.credential.id, { name: form.name.trim(), config: buildConfigPayload() });
105
- toast.success("Credential updated");
106
119
  emit("changed");
107
120
  emit("update:open", false);
121
+ toast.success("Credential updated");
108
122
  } catch (e) {
109
- toast.error(e?.message ?? "Could not save credential.");
123
+ fieldErrors.value = apiFieldErrors(e);
124
+ saveError.value = refusalReason(e, "Could not save credential.");
125
+ toast.error(saveError.value);
110
126
  } finally {
111
127
  saving.value = false;
112
128
  }
113
129
  }
130
+ const pendingDelete = ref(false);
114
131
  async function destroy() {
115
132
  if (!props.credential) return;
116
- if (!confirm(`Delete credential "${props.credential.name}"? Workflows referencing it will fail until re-bound.`)) return;
133
+ pendingDelete.value = false;
117
134
  deleting.value = true;
118
135
  try {
119
136
  await remove(props.credential.id);
@@ -208,58 +225,86 @@ async function connect() {
208
225
  <div class="space-y-3">
209
226
  <div class="space-y-1.5">
210
227
  <Label for="detail-cred-name">Name</Label>
211
- <Input id="detail-cred-name" v-model="form.name" class="bg-white" />
228
+ <Input
229
+ id="detail-cred-name"
230
+ v-model="form.name"
231
+ class="bg-white"
232
+ :aria-invalid="fieldErrors.name ? 'true' : void 0"
233
+ :aria-describedby="fieldErrors.name ? 'detail-cred-name-error' : void 0"
234
+ />
235
+ <p v-if="fieldErrors.name" id="detail-cred-name-error" class="text-sm text-rose-700">
236
+ {{ fieldErrors.name }}
237
+ </p>
212
238
  </div>
213
239
 
214
- <!-- "Use revenexx …" source picker (SFTP / Mail / API key) -->
240
+ <!-- "Use revenexx …" source picker (SFTP / Mail / API key) — mounted
241
+ only for the types it serves, which is tidiness rather than
242
+ protection: it renders nothing for any other type anyway. It used
243
+ to be protection, because the component resolved cockpit-only
244
+ sources at setup and mounting it elsewhere took this form down;
245
+ since PO-222 it asks instead (`utils/cockpitSources.ts`). -->
215
246
  <IntegrationsCredentialRevenexxSource
247
+ v-if="hasRevenexxSource(type?.slug)"
216
248
  :type="type"
217
249
  @update:active="(v) => sourceActive = v"
218
250
  @apply="(c) => Object.assign(form.config, c)"
219
251
  />
220
252
 
253
+ <!-- Each label names its own control (`for`/`id`): without it a
254
+ screen reader reads the field as unlabelled, and the label text
255
+ cannot be used to reach it at all. -->
221
256
  <div v-for="f in fieldsOf(type)" v-show="!sourceActive" :key="f.key" class="space-y-1.5">
222
- <Label>{{ loc(f.label) }}{{ f.required ? " *" : "" }}</Label>
257
+ <Label :for="`detail-field-${f.key}`">{{ resolveLocalized(f.label) }}{{ f.required ? " *" : "" }}</Label>
223
258
 
224
259
  <Input
225
260
  v-if="f.type === 'secret'"
261
+ :id="`detail-field-${f.key}`"
226
262
  :model-value="form.config[f.key] ?? ''"
227
263
  type="password"
228
264
  autocomplete="new-password"
229
265
  placeholder="Leave blank to keep current"
230
266
  class="bg-white"
267
+ v-bind="configFieldAria(f.key)"
231
268
  @update:model-value="(v) => form.config[f.key] = v"
232
269
  />
233
270
  <Input
234
271
  v-else-if="f.type === 'number'"
272
+ :id="`detail-field-${f.key}`"
235
273
  type="number"
236
274
  :model-value="form.config[f.key] ?? ''"
237
275
  class="bg-white"
276
+ v-bind="configFieldAria(f.key)"
238
277
  @update:model-value="(v) => form.config[f.key] = v === '' ? void 0 : Number(v)"
239
278
  />
240
279
  <div v-else-if="f.type === 'boolean'" class="flex items-center gap-2 pt-1">
241
- <Switch :model-value="Boolean(form.config[f.key])" @update:model-value="(v) => form.config[f.key] = v" />
280
+ <Switch :id="`detail-field-${f.key}`" :model-value="Boolean(form.config[f.key])" v-bind="configFieldAria(f.key)" @update:model-value="(v) => form.config[f.key] = v" />
242
281
  </div>
243
282
  <Select
244
283
  v-else-if="f.type === 'select'"
245
284
  :model-value="form.config[f.key] ?? ''"
246
285
  @update:model-value="(v) => form.config[f.key] = v"
247
286
  >
248
- <SelectTrigger class="bg-white">
287
+ <SelectTrigger :id="`detail-field-${f.key}`" class="bg-white" v-bind="configFieldAria(f.key)">
249
288
  <SelectValue placeholder="Pick one" />
250
289
  </SelectTrigger>
251
290
  <SelectContent>
252
291
  <SelectItem v-for="opt in f.options ?? []" :key="String(opt.value)" :value="String(opt.value)">
253
- {{ loc(opt.label) }}
292
+ {{ resolveLocalized(opt.label) }}
254
293
  </SelectItem>
255
294
  </SelectContent>
256
295
  </Select>
257
296
  <Input
258
297
  v-else
298
+ :id="`detail-field-${f.key}`"
259
299
  :model-value="form.config[f.key] ?? ''"
260
300
  class="bg-white"
301
+ v-bind="configFieldAria(f.key)"
261
302
  @update:model-value="(v) => form.config[f.key] = v"
262
303
  />
304
+
305
+ <p v-if="fieldErrors[`config.${f.key}`]" :id="`detail-field-${f.key}-error`" class="text-sm text-rose-700">
306
+ {{ fieldErrors[`config.${f.key}`] }}
307
+ </p>
263
308
  </div>
264
309
 
265
310
  <!-- Live test feedback (from the test the operator just ran) -->
@@ -271,6 +316,12 @@ async function connect() {
271
316
  {{ liveTest.ok ? "Connection ok." : liveTest.error ?? "Connection failed." }}
272
317
  </div>
273
318
 
319
+ <!-- The save was refused. Same rule as the create dialog: stated here, and at
320
+ the field where the service keyed it. -->
321
+ <div v-if="saveError" role="alert" class="rounded-md bg-rose-500/10 px-3 py-2 text-sm text-rose-700">
322
+ {{ saveError }}
323
+ </div>
324
+
274
325
  <div class="flex items-center gap-2 pt-1">
275
326
  <div class="flex-1" />
276
327
  <Button variant="outline" :disabled="testing || saving" @click="runTest">
@@ -327,7 +378,7 @@ async function connect() {
327
378
  size="sm"
328
379
  class="shrink-0 border-rose-500/40 text-rose-700 hover:bg-rose-500/10"
329
380
  :disabled="deleting"
330
- @click="destroy"
381
+ @click="pendingDelete = true"
331
382
  >
332
383
  <Loader2 v-if="deleting" :size="14" class="mr-1 animate-spin" />
333
384
  <Trash2 v-else :size="14" class="mr-1" />
@@ -337,6 +388,27 @@ async function connect() {
337
388
  </div>
338
389
  </div>
339
390
  </div>
391
+
392
+ <AlertDialog :open="pendingDelete" @update:open="(v) => pendingDelete = v">
393
+ <AlertDialogContent>
394
+ <AlertDialogHeader>
395
+ <AlertDialogTitle>Delete credential?</AlertDialogTitle>
396
+ <AlertDialogDescription>
397
+ <span class="font-medium text-foreground">{{ credential?.name }}</span>
398
+ will be removed. Workflows that reference it will fail until they are re-bound to another credential.
399
+ </AlertDialogDescription>
400
+ </AlertDialogHeader>
401
+ <AlertDialogFooter>
402
+ <Button variant="outline" :disabled="deleting" @click="pendingDelete = false">
403
+ Cancel
404
+ </Button>
405
+ <Button class="bg-destructive text-white hover:bg-destructive/90" :disabled="deleting" @click="destroy">
406
+ <Loader2 v-if="deleting" :size="14" class="animate-spin" />
407
+ {{ deleting ? "Deleting\u2026" : "Delete" }}
408
+ </Button>
409
+ </AlertDialogFooter>
410
+ </AlertDialogContent>
411
+ </AlertDialog>
340
412
  </DialogContent>
341
413
  </Dialog>
342
414
  </template>