Repository navigation
Expand file tree
/
Copy pathapp_form.go.html
More file actions
473 lines (452 loc) · 21 KB
/
Copy pathapp_form.go.html
File metadata and controls
473 lines (452 loc) · 21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
{{/* Copyright (c) ClaceIO, LLC */}}
{{/* SPDX-License-Identifier: Apache-2.0 */}}
{{/* App create/update form (/apps/create, /apps/update). Create supports a
Validate (dry run) step and a post-create permission approval step */}}
{{ template "page_open" (dict "Ctx" . "Main" "space-y-4 max-w-2xl") }}
<!-- Header -->
<div class="page-header flex items-center gap-2 flex-wrap mb-2 md:mb-6">
{{ template "hamburger" . }}
<div>
{{ template "page_title" "Apps" }}
<nav aria-label="Breadcrumb" class="breadcrumbs text-xs text-base-content/70 p-0"><ul class="flex-wrap whitespace-normal"><li><a href="{{ .AppPath }}/apps">Apps</a></li>{{ if eq .Data.Mode "update" }}<li class="font-mono break-all"><a href="{{ .AppPath }}/apps/detail?path={{ .Data.Values.path }}">{{ .Data.Values.path }}</a></li><li>edit</li>{{ else }}<li>new</li>{{ end }}</ul></nav>
<h2 class="text-xl font-semibold">
{{ if eq .Data.Mode "create" }}
New app
{{ else }}
Update
<span class="font-mono break-all">{{ .Data.Values.path }}</span>
{{ end }}
</h2>
<p class="page-desc text-sm text-base-content/70">
{{ if eq .Data.Step "approve" }}
Approve the permissions the app requests
{{ else if eq .Data.Mode "create" }}
Deploy an app from a git repo or a local directory
{{ else if and .Data.App .Data.App.staged_changes }}
Editing the staged configuration - changes stay in staging
until promoted
{{ else }}
Changes apply to staging and are promoted to prod
{{ end }}
</p>
</div>
</div>
{{ template "page_progress" }}
{{ block "op_form" . }}
<div id="op-form-wrap" class="space-y-4">
{{ template "error_alert" .Data.Error }}
{{ if eq .Data.Step "approve" }}
<!-- Approve step: the app was created, its permissions are pending -->
{{ if not .Data.Error }}
<div
role="alert"
class="alert alert-success alert-soft py-2 text-sm">
<span aria-hidden="true">✓</span>
<span
><span class="font-mono break-all">{{ .Data.Values.path }}</span> was
created{{ if .Data.CreatedAuth }} with login
<span class="font-mono" data-created-auth="{{ .Data.CreatedAuth }}">{{ .Data.CreatedAuth }}</span>{{ if eq .Data.CreatedAuth "none" }}
(reachable by anyone){{ end }}{{ end }} - it needs the permissions below approved before it
can serve requests</span
>
</div>
{{ end }}
{{ template "perms_card" (dict "Review" .Data.Review "Note" "Approving grants the plugin and permission usage listed above") }}
<form
method="post"
action="{{ .AppPath }}/apps/create"
id="op-form"
hx-post="{{ .AppPath }}/apps/create"
hx-target="#op-form-wrap"
hx-swap="outerHTML show:top showTarget:#op-form-wrap"
hx-indicator="#page-progress, #op-form"
class="flex justify-end gap-2">
<input type="hidden" name="path" value="{{ .Data.Values.path }}" />
<a href="{{ .AppPath }}/apps" class="btn btn-ghost btn-sm"
>Approve later</a
>
<button
type="submit"
name="action"
value="approve"
{{ if not (or (index .Data.Perms "app:approve") (index .Data.Perms "admin")) }}
disabled data-tip="requires the app:approve permission"
{{ end }}
class="btn btn-primary btn-sm transition-transform duration-150 active:scale-95 {{ if not (or (index .Data.Perms "app:approve") (index .Data.Perms "admin")) }}tooltip tooltip-top{{ end }}">
Approve permissions
</button>
</form>
{{ else if and (eq .Data.Mode "update") (not .Data.App) }}
<!-- App could not be loaded -->
<div class="card bg-base-100 border border-base-300 py-10">
<div
class="flex flex-col items-center gap-2 text-center text-sm text-base-content/70">
<span class="text-xl" aria-hidden="true">🔍</span>
<p>The app could not be loaded</p>
<a class="btn btn-xs btn-outline" href="{{ .AppPath }}/apps"
>Back to apps</a
>
</div>
</div>
{{ else }}
{{ template "validation_alert" (dict
"Validated" .Data.Validated
"Value" .Data.Values.path
"Suffix" "is ready to be created"
"BreakAll" true) }}
{{ if .Data.Validated }}
{{ template "perms_card" (dict "Review" .Data.Review "Note" "These permissions will need approval when the app is created") }}
{{ end }}
{{ if eq .Data.Mode "update" }}
<!-- Current app summary -->
<div class="card bg-base-100 border border-base-300">
<div
class="px-4 py-3 grid grid-cols-3 gap-x-6 gap-y-2 text-sm">
{{/* The source (a long path or git url) takes its own
row: as one of four columns it wrapped every few
characters */}}
<div class="col-span-3">
<div class="text-xs text-base-content/70">Source</div>
<div
class="font-mono text-xs break-all"
title="{{ .Data.App.source_url }}">
{{ .Data.App.source_url }}
</div>
</div>
<div>
<div class="text-xs text-base-content/70">Version</div>
<div class="font-mono text-xs">
v{{ .Data.App.version }}
</div>
</div>
<div>
<div class="text-xs text-base-content/70">Spec</div>
<div class="font-mono text-xs break-all">
{{ if .Data.App.spec }}{{ .Data.App.spec }}{{ else }}-{{ end }}
</div>
</div>
<div>
<div class="text-xs text-base-content/70">Branch</div>
<div class="font-mono text-xs break-all">
{{ if .Data.App.git_branch }}
{{ .Data.App.git_branch }}
{{ else }}
-
{{ end }}
</div>
</div>
</div>
</div>
{{ end }}
<form
method="post"
action="{{ .AppPath }}/apps/{{ .Data.Mode }}"
id="op-form"
hx-post="{{ .AppPath }}/apps/{{ .Data.Mode }}"
hx-target="#op-form-wrap"
hx-swap="outerHTML show:top showTarget:#op-form-wrap"
hx-indicator="#page-progress, #op-form"
class="card bg-base-100 border border-base-300">
<div class="card-body p-5 gap-5">
{{ if eq .Data.Mode "create" }}
<div class="space-y-4">
<div>
<label
class="block text-sm font-medium mb-1"
for="app-path"
>App path</label
>
<input
id="app-path"
name="path"
type="text"
required
value="{{ .Data.Values.path }}"
placeholder="/myapp or crm.example.com:/"
class="input w-full font-mono text-sm" />
<p class="text-xs text-base-content/70 mt-1">
Path where the app is served, optionally with a domain
</p>
</div>
<div>
<label
class="block text-sm font-medium mb-1"
for="app-source"
>Source</label
>
<input
id="app-source"
name="source_url"
type="text"
required
value="{{ .Data.Values.source_url }}"
placeholder="github.com/org/repo/folder or /local/dir"
class="input w-full font-mono text-sm" />
<p class="text-xs text-base-content/70 mt-1">
Git url or a directory on the server
</p>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label
class="block text-sm font-medium mb-1"
for="app-branch"
>Git branch</label
>
<input
id="app-branch"
name="git_branch"
type="text"
value="{{ .Data.Values.git_branch }}"
placeholder="main"
class="input w-full font-mono text-sm" />
</div>
<div>
<label
class="block text-sm font-medium mb-1"
for="app-git-auth"
>Git auth</label
>
<select
id="app-git-auth"
name="git_auth"
class="select w-full font-mono text-sm">
{{ $gitAuth := .Data.Values.git_auth }}
<option
value=""
{{ if not $gitAuth }}selected{{ end }}>
none (public repo)
</option>
{{ template "select_options" (dict "Options" .Data.GitAuthOptions "Current" $gitAuth) }}
</select>
</div>
</div>
<div>
<label
class="block text-sm font-medium mb-1"
for="app-spec"
>Spec</label
>
<select
id="app-spec"
name="spec"
class="select w-full font-mono text-sm">
{{ $spec := .Data.Values.spec }}
<option value="" {{ if not $spec }}selected{{ end }}>
none (source has app.star)
</option>
{{ template "select_options" (dict "Options" .Data.Specs "Current" $spec) }}
</select>
<p class="text-xs text-base-content/70 mt-1">
App spec to use when the source has no app.star, for
example python-flask or static
</p>
</div>
</div>
<div class="divider my-0"></div>
{{ end }}
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-1" for="app-auth"
>Authentication</label
>
<select
id="app-auth"
name="auth"
class="select w-full sm:w-1/2">
{{ $current := .Data.Values.auth }}
{{ template "select_options" (dict "Options" .Data.AuthOptions "Current" $current) }}
</select>
<p class="text-xs text-base-content/70 mt-1">
default uses the server's app_default_auth setting{{ if .Data.DefaultAuth }}
(<span class="font-mono">{{ .Data.DefaultAuth }}</span> on this server){{ end }}{{ if eq .Data.Mode "create" }};
the app is reachable with that login as soon as it is created{{ end }}
</p>
</div>
<div>
<label class="block text-sm font-medium mb-1" for="app-mcp"
>MCP</label
>
{{ $mcp := or .Data.Values.mcp "default" }}
<select
id="app-mcp"
name="mcp"
class="select w-full">
{{ range .Data.McpOptions }}
<option
value="{{ .value }}"
{{ if eq .value $mcp }}selected{{ end }}>
{{ .label }}
</option>
{{ end }}
{{ if eq $mcp "custom" }}
<option value="custom" selected>
custom - keep the MCP document set from the CLI
</option>
{{ end }}
</select>
<p class="text-xs text-base-content/70 mt-1">
An app with actions serves them as MCP tools by default, on
its own <span class="font-mono">/mcp</span> url and through
the server's app actions endpoint. Disable keeps this app out
of every tool list.
{{ if and (index .Data.Perms "feature:config") (index .Data.Perms "config:read") }}
<a href="{{ .AppPath }}/config/mcp" class="link text-primary">MCP settings →</a>
{{ end }}
</p>
</div>
<div>
<span class="block text-sm font-medium mb-1">Parameters</span>
{{ template "kv_table" (dict "Field" "params"
"AppPath" .AppPath
"Prefix" "appparam"
"CanCreate" (index .Data.Perms "secret:create")
"CanDelete" (index .Data.Perms "secret:delete")
"Rows" .Data.Values.params_rows
"KeyPlaceholder" "SMTP_HOST"
"ValuePlaceholder" "smtp.example.com"
"AddLabel" "Add parameter") }}
<p class="text-xs text-base-content/70 mt-1">
The lock button encrypts a value into the secrets store
and uses the secret reference instead.
{{ if eq .Data.Mode "update" }}
Removing a row deletes the parameter.
{{ end }}
</p>
</div>
<div>
<span class="block text-sm font-medium mb-1"
>Service bindings</span
>
{{ $opts := .Data.BindingOptions }}
<div class="binding-table space-y-3">
{{ range .Data.Values.bindings }}
{{ template "binding_row" (dict "Options" $opts "Selected" .) }}
{{ end }}
<template>
{{ template "binding_row" (dict "Options" $opts) }}
</template>
{{ if or $opts.services $opts.bindings .Data.Values.bindings }}
<button
type="button"
class="btn btn-outline btn-xs"
onclick="addBindingRow(this)">
+ Add binding
</button>
{{ else }}
<p class="text-xs text-base-content/70">
No services or bindings are configured
</p>
{{ end }}
</div>
<p class="text-xs text-base-content/70 mt-1">
Bind the app to a service (an auto binding is created
for the app) or to an existing binding
</p>
</div>
{{ if eq .Data.Mode "update" }}
<input
type="hidden"
name="path"
value="{{ .Data.Values.path }}" />
{{ end }}
</div>
<div class="card-actions justify-end pt-2">
<a
href="{{ if eq .Data.Mode "update" }}{{ .AppPath }}/apps/detail?path={{ .Data.Values.path }}{{ else }}{{ .AppPath }}/apps{{ end }}"
class="btn btn-ghost btn-sm"
>Cancel</a
>
{{ if eq .Data.Mode "create" }}
<button
type="submit"
name="action"
value="validate"
{{ if index .Data.Perms "app:create" }}
data-tip="Dry run: check the create and show the requested permissions without creating"
{{ else }}
disabled data-tip="requires app:create"
{{ end }}
class="btn btn-outline btn-sm transition-transform duration-150 active:scale-95 tooltip tooltip-top">
Validate
</button>
<button
type="submit"
name="action"
value="create"
{{ if not (index .Data.Perms "app:create") }}
disabled data-tip="requires app:create"
{{ end }}
class="btn btn-primary btn-sm transition-transform duration-150 active:scale-95 {{ if not (index .Data.Perms "app:create") }}tooltip tooltip-top{{ end }}">
Create app
</button>
{{ else }}
<button
type="submit"
{{ if not (index .Data.Perms "app:update") }}
disabled data-tip="requires app:update"
{{ end }}
class="btn btn-primary btn-sm transition-transform duration-150 active:scale-95 {{ if not (index .Data.Perms "app:update") }}tooltip tooltip-top{{ end }}">
Save changes
</button>
{{ end }}
</div>
</div>
</form>
{{ end }}
</div>
{{ end }}
{{ template "page_close" . }}
{{/* binding_row is one service bindings row: a select over the configured
services (binding to a service creates an auto binding for the app) and
the existing base/derived binding paths. Context: dict with Options (the
BindingOptions lists) and optional Selected. A selected value missing from
the lists (e.g. a CLI-created reference) is kept as an extra option
*/}}
{{ define "binding_row" }}
{{ $sel := or .Selected "" }}
{{ $found := not $sel }}
<div class="binding-row flex gap-2 items-start">
{{/* min-w-0: a select's min-content width is its widest option text
(long binding paths), which would defeat flex shrinking on mobile */}}
<select
name="bindings"
aria-label="Service or binding"
class="select w-full min-w-0 font-mono text-sm">
<option value="" {{ if not $sel }}selected{{ end }}>
select a service or binding
</option>
{{ if .Options.services }}
<optgroup label="Services (creates an auto binding)">
{{ range .Options.services }}
<option value="{{ . }}" {{ if eq . $sel }}selected{{ end }}>
{{ . }}
</option>
{{ if eq . $sel }}{{ $found = true }}{{ end }}
{{ end }}
</optgroup>
{{ end }}
{{ if .Options.bindings }}
<optgroup label="Existing bindings">
{{ range .Options.bindings }}
<option value="{{ . }}" {{ if eq . $sel }}selected{{ end }}>
{{ . }}
</option>
{{ if eq . $sel }}{{ $found = true }}{{ end }}
{{ end }}
</optgroup>
{{ end }}
{{ if not $found }}
<option value="{{ $sel }}" selected>{{ $sel }}</option>
{{ end }}
</select>
<button
type="button"
class="btn btn-ghost btn-square tooltip tooltip-top"
data-tip="Remove binding"
aria-label="Remove binding"
onclick="removeBindingRow(this)">
✕
</button>
</div>
{{ end }}