Signed-off-by: oppiliappan me@oppi.li
+44
appview/pages/pages.go
+44
appview/pages/pages.go
···
702
return p.executeRepo("repo/settings", w, params)
703
}
704
705
+
type RepoGeneralSettingsParams struct {
706
+
LoggedInUser *oauth.User
707
+
RepoInfo repoinfo.RepoInfo
708
+
Active string
709
+
Tabs []map[string]any
710
+
Tab string
711
+
Branches []types.Branch
712
+
}
713
+
714
+
func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error {
715
+
params.Active = "settings"
716
+
return p.executeRepo("repo/settings/general", w, params)
717
+
}
718
+
719
+
type RepoAccessSettingsParams struct {
720
+
LoggedInUser *oauth.User
721
+
RepoInfo repoinfo.RepoInfo
722
+
Active string
723
+
Tabs []map[string]any
724
+
Tab string
725
+
Collaborators []Collaborator
726
+
}
727
+
728
+
func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error {
729
+
params.Active = "settings"
730
+
return p.executeRepo("repo/settings/access", w, params)
731
+
}
732
+
733
+
type RepoPipelineSettingsParams struct {
734
+
LoggedInUser *oauth.User
735
+
RepoInfo repoinfo.RepoInfo
736
+
Active string
737
+
Tabs []map[string]any
738
+
Tab string
739
+
Spindles []string
740
+
CurrentSpindle string
741
+
Secrets []map[string]any
742
+
}
743
+
744
+
func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error {
745
+
params.Active = "settings"
746
+
return p.executeRepo("repo/settings/pipelines", w, params)
747
+
}
748
+
749
type RepoIssuesParams struct {
750
LoggedInUser *oauth.User
751
RepoInfo repoinfo.RepoInfo
+146
-160
appview/pages/templates/repo/settings.html
+146
-160
appview/pages/templates/repo/settings.html
···
1
{{ define "title" }}settings · {{ .RepoInfo.FullName }}{{ end }}
2
{{ define "repoContent" }}
3
-
<header class="font-bold text-sm mb-4 uppercase dark:text-white">
4
-
Collaborators
5
-
</header>
6
7
-
<div id="collaborator-list" class="flex flex-col gap-2 mb-2">
8
-
{{ range .Collaborators }}
9
-
<div id="collaborator" class="mb-2">
10
-
<a
11
-
href="/{{ didOrHandle .Did .Handle }}"
12
-
class="no-underline hover:underline text-black dark:text-white"
13
-
>
14
-
{{ didOrHandle .Did .Handle }}
15
-
</a>
16
-
<div>
17
-
<span class="text-sm text-gray-500 dark:text-gray-400">
18
-
{{ .Role }}
19
-
</span>
20
-
</div>
21
-
</div>
22
-
{{ end }}
23
-
</div>
24
25
-
{{ if .RepoInfo.Roles.CollaboratorInviteAllowed }}
26
-
<form
27
-
hx-put="/{{ $.RepoInfo.FullName }}/settings/collaborator"
28
-
class="group"
29
>
30
-
<label for="collaborator" class="dark:text-white">
31
-
add collaborator
32
-
</label>
33
-
<input
34
-
type="text"
35
-
id="collaborator"
36
-
name="collaborator"
37
-
required
38
-
class="dark:bg-gray-700 dark:text-white"
39
-
placeholder="enter did or handle"
40
-
>
41
-
<button
42
-
class="btn my-2 flex gap-2 items-center dark:text-white dark:hover:bg-gray-700"
43
-
type="text"
44
-
>
45
-
<span>add</span>
46
-
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
47
-
</button>
48
-
</form>
49
{{ end }}
50
51
<form
52
-
hx-put="/{{ $.RepoInfo.FullName }}/settings/branches/default"
53
-
class="mt-6 group"
54
>
55
-
<label for="branch">default branch</label>
56
-
<div class="flex gap-2 items-center">
57
-
<select id="branch" name="branch" required class="p-1 border border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700">
58
-
<option
59
-
value=""
60
-
disabled
61
-
selected
62
-
>
63
-
Choose a default branch
64
-
</option>
65
-
{{ range .Branches }}
66
-
<option
67
-
value="{{ .Name }}"
68
-
class="py-1"
69
-
{{ if .IsDefault }}
70
-
selected
71
-
{{ end }}
72
-
>
73
-
{{ .Name }}
74
-
</option>
75
-
{{ end }}
76
-
</select>
77
-
<button class="btn my-2 flex gap-2 items-center" type="submit">
78
-
<span>save</span>
79
-
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
80
-
</button>
81
-
</div>
82
</form>
83
84
-
{{ if .RepoInfo.Roles.IsOwner }}
85
<form
86
-
hx-post="/{{ $.RepoInfo.FullName }}/settings/spindle"
87
-
class="mt-6 group"
88
-
>
89
-
<label for="spindle">spindle</label>
90
-
<div class="flex gap-2 items-center">
91
-
<select id="spindle" name="spindle" required class="p-1 border border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700">
92
-
<option
93
-
value=""
94
-
selected
95
-
>
96
-
None
97
-
</option>
98
-
{{ range .Spindles }}
99
-
<option
100
-
value="{{ . }}"
101
-
class="py-1"
102
-
{{ if eq . $.CurrentSpindle }}
103
-
selected
104
-
{{ end }}
105
-
>
106
-
{{ . }}
107
-
</option>
108
-
{{ end }}
109
-
</select>
110
-
<button class="btn my-2 flex gap-2 items-center" type="submit">
111
-
<span>save</span>
112
-
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
113
-
</button>
114
-
</div>
115
</form>
116
-
{{ end }}
117
118
-
{{ if $.CurrentSpindle }}
119
-
<header class="font-bold text-sm mb-4 uppercase dark:text-white">
120
-
Secrets
121
-
</header>
122
123
-
<div id="secret-list" class="flex flex-col gap-2 mb-2">
124
-
{{ range $idx, $secret := .Secrets }}
125
-
{{ with $secret }}
126
-
<div id="secret-{{$idx}}" class="mb-2">
127
-
{{ .Key }} created on {{ .CreatedAt }} by {{ .CreatedBy }}
128
-
</div>
129
-
{{ end }}
130
{{ end }}
131
-
</div>
132
-
<form
133
-
hx-put="/{{ $.RepoInfo.FullName }}/settings/secrets"
134
-
class="mt-6"
135
-
hx-indicator="#add-secret-spinner">
136
-
<label for="key">secret key</label>
137
-
<input
138
-
type="text"
139
-
id="key"
140
-
name="key"
141
-
required
142
-
class="dark:bg-gray-700 dark:text-white"
143
-
placeholder="SECRET_KEY" />
144
-
<label for="value">secret value</label>
145
-
<input
146
-
type="text"
147
-
id="value"
148
-
name="value"
149
-
required
150
-
class="dark:bg-gray-700 dark:text-white"
151
-
placeholder="SECRET VALUE" />
152
153
-
<button class="btn my-2 flex items-center" type="text">
154
-
<span>add</span>
155
-
<span id="add-secret-spinner" class="group">
156
-
{{ i "loader-circle" "ml-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
157
-
</span>
158
-
</button>
159
-
</form>
160
-
{{ end }}
161
162
-
{{ if .RepoInfo.Roles.RepoDeleteAllowed }}
163
<form
164
-
hx-confirm="Are you sure you want to delete this repository?"
165
-
hx-delete="/{{ $.RepoInfo.FullName }}/settings/delete"
166
class="mt-6"
167
-
hx-indicator="#delete-repo-spinner"
168
-
>
169
-
<label for="branch">delete repository</label>
170
-
<button class="btn my-2 flex items-center" type="text">
171
-
<span>delete</span>
172
-
<span id="delete-repo-spinner" class="group">
173
-
{{ i "loader-circle" "ml-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
174
-
</span>
175
-
</button>
176
-
<span>
177
-
Deleting a repository is irreversible and permanent.
178
-
</span>
179
-
</form>
180
-
{{ end }}
181
182
{{ end }}
···
1
{{ define "title" }}settings · {{ .RepoInfo.FullName }}{{ end }}
2
+
3
{{ define "repoContent" }}
4
+
{{ template "collaboratorSettings" . }}
5
+
{{ template "branchSettings" . }}
6
+
{{ template "dangerZone" . }}
7
+
{{ template "spindleSelector" . }}
8
+
{{ template "spindleSecrets" . }}
9
+
{{ end }}
10
11
+
{{ define "collaboratorSettings" }}
12
+
<header class="font-bold text-sm mb-4 uppercase dark:text-white">
13
+
Collaborators
14
+
</header>
15
16
+
<div id="collaborator-list" class="flex flex-col gap-2 mb-2">
17
+
{{ range .Collaborators }}
18
+
<div id="collaborator" class="mb-2">
19
+
<a
20
+
href="/{{ didOrHandle .Did .Handle }}"
21
+
class="no-underline hover:underline text-black dark:text-white"
22
>
23
+
{{ didOrHandle .Did .Handle }}
24
+
</a>
25
+
<div>
26
+
<span class="text-sm text-gray-500 dark:text-gray-400">
27
+
{{ .Role }}
28
+
</span>
29
+
</div>
30
+
</div>
31
{{ end }}
32
+
</div>
33
34
+
{{ if .RepoInfo.Roles.CollaboratorInviteAllowed }}
35
<form
36
+
hx-put="/{{ $.RepoInfo.FullName }}/settings/collaborator"
37
+
class="group"
38
>
39
+
<label for="collaborator" class="dark:text-white">
40
+
add collaborator
41
+
</label>
42
+
<input
43
+
type="text"
44
+
id="collaborator"
45
+
name="collaborator"
46
+
required
47
+
class="dark:bg-gray-700 dark:text-white"
48
+
placeholder="enter did or handle">
49
+
<button class="btn my-2 flex gap-2 items-center dark:text-white dark:hover:bg-gray-700" type="text">
50
+
<span>add</span>
51
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
52
+
</button>
53
</form>
54
+
{{ end }}
55
+
{{ end }}
56
57
+
{{ define "dangerZone" }}
58
+
{{ if .RepoInfo.Roles.RepoDeleteAllowed }}
59
<form
60
+
hx-confirm="Are you sure you want to delete this repository?"
61
+
hx-delete="/{{ $.RepoInfo.FullName }}/settings/delete"
62
+
class="mt-6"
63
+
hx-indicator="#delete-repo-spinner">
64
+
<label for="branch">delete repository</label>
65
+
<button class="btn my-2 flex items-center" type="text">
66
+
<span>delete</span>
67
+
<span id="delete-repo-spinner" class="group">
68
+
{{ i "loader-circle" "ml-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
69
+
</span>
70
+
</button>
71
+
<span>
72
+
Deleting a repository is irreversible and permanent.
73
+
</span>
74
</form>
75
+
{{ end }}
76
+
{{ end }}
77
78
+
{{ define "branchSettings" }}
79
+
<form hx-put="/{{ $.RepoInfo.FullName }}/settings/branches/default" class="mt-6 group">
80
+
<label for="branch">default branch</label>
81
+
<div class="flex gap-2 items-center">
82
+
<select id="branch" name="branch" required class="p-1 border border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700">
83
+
<option value="" disabled selected >
84
+
Choose a default branch
85
+
</option>
86
+
{{ range .Branches }}
87
+
<option value="{{ .Name }}" class="py-1" {{ if .IsDefault }}selected{{ end }} >
88
+
{{ .Name }}
89
+
</option>
90
+
{{ end }}
91
+
</select>
92
+
<button class="btn my-2 flex gap-2 items-center" type="submit">
93
+
<span>save</span>
94
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
95
+
</button>
96
+
</div>
97
+
</form>
98
+
{{ end }}
99
100
+
{{ define "spindleSelector" }}
101
+
{{ if .RepoInfo.Roles.IsOwner }}
102
+
<form hx-post="/{{ $.RepoInfo.FullName }}/settings/spindle" class="mt-6 group" >
103
+
<label for="spindle">spindle</label>
104
+
<div class="flex gap-2 items-center">
105
+
<select id="spindle" name="spindle" required class="p-1 border border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700">
106
+
<option value="" selected >
107
+
None
108
+
</option>
109
+
{{ range .Spindles }}
110
+
<option value="{{ . }}" class="py-1" {{ if eq . $.CurrentSpindle }}selected{{ end }}>
111
+
{{ . }}
112
+
</option>
113
{{ end }}
114
+
</select>
115
+
<button class="btn my-2 flex gap-2 items-center" type="submit">
116
+
<span>save</span>
117
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
118
+
</button>
119
+
</div>
120
+
</form>
121
+
{{ end }}
122
+
{{ end }}
123
124
+
{{ define "spindleSecrets" }}
125
+
{{ if $.CurrentSpindle }}
126
+
<header class="font-bold text-sm mb-4 uppercase dark:text-white">
127
+
Secrets
128
+
</header>
129
130
+
<div id="secret-list" class="flex flex-col gap-2 mb-2">
131
+
{{ range $idx, $secret := .Secrets }}
132
+
{{ with $secret }}
133
+
<div id="secret-{{$idx}}" class="mb-2">
134
+
{{ .Key }} created on {{ .CreatedAt }} by {{ .CreatedBy }}
135
+
</div>
136
+
{{ end }}
137
+
{{ end }}
138
+
</div>
139
<form
140
+
hx-put="/{{ $.RepoInfo.FullName }}/settings/secrets"
141
class="mt-6"
142
+
hx-indicator="#add-secret-spinner">
143
+
<label for="key">secret key</label>
144
+
<input
145
+
type="text"
146
+
id="key"
147
+
name="key"
148
+
required
149
+
class="dark:bg-gray-700 dark:text-white"
150
+
placeholder="SECRET_KEY" />
151
+
<label for="value">secret value</label>
152
+
<input
153
+
type="text"
154
+
id="value"
155
+
name="value"
156
+
required
157
+
class="dark:bg-gray-700 dark:text-white"
158
+
placeholder="SECRET VALUE" />
159
160
+
<button class="btn my-2 flex items-center" type="text">
161
+
<span>add</span>
162
+
<span id="add-secret-spinner" class="group">
163
+
{{ i "loader-circle" "ml-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
164
+
</span>
165
+
</button>
166
+
</form>
167
+
{{ end }}
168
{{ end }}
+110
appview/pages/templates/repo/settings/access.html
+110
appview/pages/templates/repo/settings/access.html
···
···
1
+
{{ define "title" }}{{ .Tab }} settings · {{ .RepoInfo.FullName }}{{ end }}
2
+
3
+
{{ define "repoContent" }}
4
+
<section class="w-full grid grid-cols-1 md:grid-cols-4 gap-2">
5
+
<div class="col-span-1">
6
+
{{ template "repo/settings/fragments/sidebar" . }}
7
+
</div>
8
+
<div class="col-span-1 md:col-span-3 flex flex-col gap-6 p-2">
9
+
{{ template "collaboratorSettings" . }}
10
+
</div>
11
+
</section>
12
+
{{ end }}
13
+
14
+
{{ define "collaboratorSettings" }}
15
+
<div class="grid grid-cols-1 gap-4 items-center">
16
+
<div class="col-span-1">
17
+
<h2 class="text-sm pb-2 uppercase font-bold">Collaborators</h2>
18
+
<p class="text-gray-500 dark:text-gray-400">
19
+
Any user added as a collaborator will be able to push commits and tags to this repository, upload releases, and workflows.
20
+
</p>
21
+
</div>
22
+
{{ template "collaboratorsGrid" . }}
23
+
</div>
24
+
{{ end }}
25
+
26
+
{{ define "collaboratorsGrid" }}
27
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
28
+
{{ if .RepoInfo.Roles.CollaboratorInviteAllowed }}
29
+
{{ template "addCollaboratorButton" . }}
30
+
{{ end }}
31
+
{{ range .Collaborators }}
32
+
<div class="border border-gray-200 dark:border-gray-700 rounded p-4">
33
+
<div class="flex items-center gap-3">
34
+
<img
35
+
src="{{ fullAvatar .Handle }}"
36
+
alt="{{ .Handle }}"
37
+
class="rounded-full h-10 w-10 border border-gray-300 dark:border-gray-600 flex-shrink-0"/>
38
+
39
+
<div class="flex-1 min-w-0">
40
+
<a href="/{{ .Handle }}" class="block truncate">
41
+
{{ didOrHandle .Did .Handle }}
42
+
</a>
43
+
<p class="text-sm text-gray-500 dark:text-gray-400">{{ .Role }}</p>
44
+
</div>
45
+
</div>
46
+
</div>
47
+
{{ end }}
48
+
</div>
49
+
{{ end }}
50
+
51
+
{{ define "addCollaboratorButton" }}
52
+
<button
53
+
class="btn block rounded p-4"
54
+
popovertarget="add-collaborator-modal"
55
+
popovertargetaction="toggle">
56
+
<div class="flex items-center gap-3">
57
+
<div class="w-10 h-10 rounded-full bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
58
+
{{ i "user-plus" "size-4" }}
59
+
</div>
60
+
61
+
<div class="text-left flex-1 min-w-0 block truncate">
62
+
Add collaborator
63
+
</div>
64
+
</div>
65
+
</button>
66
+
<div
67
+
id="add-collaborator-modal"
68
+
popover
69
+
class="bg-white w-full md:w-96 dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 drop-shadow dark:text-white backdrop:bg-gray-400/50 dark:backdrop:bg-gray-800/50">
70
+
{{ template "addCollaboratorModal" . }}
71
+
</div>
72
+
{{ end }}
73
+
74
+
{{ define "addCollaboratorModal" }}
75
+
<form
76
+
hx-put="/{{ $.RepoInfo.FullName }}/settings/collaborator"
77
+
hx-indicator="#spinner"
78
+
hx-swap="none"
79
+
class="flex flex-col gap-2"
80
+
>
81
+
<label for="add-collaborator" class="uppercase p-0">
82
+
ADD COLLABORATOR
83
+
</label>
84
+
<p class="text-sm text-gray-500 dark:text-gray-400">Collaborators can push to this repository.</p>
85
+
<input
86
+
type="text"
87
+
id="add-collaborator"
88
+
name="collaborator"
89
+
required
90
+
placeholder="@foo.bsky.social"
91
+
/>
92
+
<div class="flex gap-2 pt-2">
93
+
<button
94
+
type="button"
95
+
popovertarget="add-collaborator-modal"
96
+
popovertargetaction="hide"
97
+
class="btn w-1/2 flex items-center gap-2 text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
98
+
>
99
+
{{ i "x" "size-4" }} cancel
100
+
</button>
101
+
<button type="submit" class="btn w-1/2 flex items-center">
102
+
<span class="inline-flex gap-2 items-center">{{ i "user-plus" "size-4" }} add</span>
103
+
<span id="spinner" class="group">
104
+
{{ i "loader-circle" "ml-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
105
+
</span>
106
+
</button>
107
+
</div>
108
+
<div id="add-collaborator-error" class="text-red-500 dark:text-red-400"></div>
109
+
</form>
110
+
{{ end }}
+29
appview/pages/templates/repo/settings/fragments/secretListing.html
+29
appview/pages/templates/repo/settings/fragments/secretListing.html
···
···
1
+
{{ define "repo/settings/fragments/secretListing" }}
2
+
{{ $root := index . 0 }}
3
+
{{ $secret := index . 1 }}
4
+
<div id="secret-{{$secret.Key}}" class="flex items-center justify-between p-2">
5
+
<div class="hover:no-underline flex flex-col gap-1 text-sm min-w-0 max-w-[80%]">
6
+
<span class="font-mono">
7
+
{{ $secret.Key }}
8
+
</span>
9
+
<div class="flex flex-wrap text items-center gap-1 text-gray-500 dark:text-gray-400">
10
+
<span>added by</span>
11
+
<span>{{ template "user/fragments/picHandleLink" $secret.CreatedBy }}</span>
12
+
<span class="before:content-['·'] before:select-none"></span>
13
+
<span>{{ template "repo/fragments/shortTimeAgo" $secret.CreatedAt }}</span>
14
+
</div>
15
+
</div>
16
+
<button
17
+
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
18
+
title="Delete secret"
19
+
hx-delete="/{{ $root.RepoInfo.FullName }}/settings/secrets"
20
+
hx-swap="none"
21
+
hx-vals='{"key": "{{ $secret.Key }}"}'
22
+
hx-confirm="Are you sure you want to delete the secret {{ $secret.Key }}?"
23
+
>
24
+
{{ i "trash-2" "w-5 h-5" }}
25
+
<span class="hidden md:inline">delete</span>
26
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
27
+
</button>
28
+
</div>
29
+
{{ end }}
+68
appview/pages/templates/repo/settings/general.html
+68
appview/pages/templates/repo/settings/general.html
···
···
1
+
{{ define "title" }}{{ .Tab }} settings · {{ .RepoInfo.FullName }}{{ end }}
2
+
3
+
{{ define "repoContent" }}
4
+
<section class="w-full grid grid-cols-1 md:grid-cols-4 gap-2">
5
+
<div class="col-span-1">
6
+
{{ template "repo/settings/fragments/sidebar" . }}
7
+
</div>
8
+
<div class="col-span-1 md:col-span-3 flex flex-col gap-6 p-2">
9
+
{{ template "branchSettings" . }}
10
+
{{ template "deleteRepo" . }}
11
+
</div>
12
+
</section>
13
+
{{ end }}
14
+
15
+
{{ define "branchSettings" }}
16
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center">
17
+
<div class="col-span-1 md:col-span-2">
18
+
<h2 class="text-sm pb-2 uppercase font-bold">Default Branch</h2>
19
+
<p class="text-gray-500 dark:text-gray-400">
20
+
The default branch is considered the “base” branch in your repository,
21
+
against which all pull requests and code commits are automatically made,
22
+
unless you specify a different branch.
23
+
</p>
24
+
</div>
25
+
<form hx-put="/{{ $.RepoInfo.FullName }}/settings/branches/default" class="col-span-1 md:col-span-1 md:justify-self-end group flex gap-2 items-stretch">
26
+
<select id="branch" name="branch" required class="p-1 max-w-64 border border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700">
27
+
<option value="" disabled selected >
28
+
Choose a default branch
29
+
</option>
30
+
{{ range .Branches }}
31
+
<option value="{{ .Name }}" class="py-1" {{ if .IsDefault }}selected{{ end }} >
32
+
{{ .Name }}
33
+
</option>
34
+
{{ end }}
35
+
</select>
36
+
<button class="btn flex gap-2 items-center" type="submit">
37
+
{{ i "check" "size-4" }}
38
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
39
+
</button>
40
+
</form>
41
+
</div>
42
+
{{ end }}
43
+
44
+
{{ define "deleteRepo" }}
45
+
{{ if .RepoInfo.Roles.RepoDeleteAllowed }}
46
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center">
47
+
<div class="col-span-1 md:col-span-2">
48
+
<h2 class="text-sm pb-2 uppercase text-red-500 dark:text-red-400 font-bold">Delete Repository</h2>
49
+
<p class="text-red-500 dark:text-red-400 ">
50
+
Deleting a repository is irreversible and permanent. Be certain before deleting a repository.
51
+
</p>
52
+
</div>
53
+
<div class="col-span-1 md:col-span-1 md:justify-self-end">
54
+
<button
55
+
class="btn group text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 flex gap-2 items-center"
56
+
type="button"
57
+
hx-delete="/{{ $.RepoInfo.FullName }}/settings/delete"
58
+
hx-confirm="Are you sure you want to delete {{ $.RepoInfo.FullName }}?">
59
+
{{ i "trash-2" "size-4" }}
60
+
delete
61
+
<span class="ml-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline">
62
+
{{ i "loader-circle" "w-4 h-4" }}
63
+
</span>
64
+
</button>
65
+
</div>
66
+
</div>
67
+
{{ end }}
68
+
{{ end }}
+135
appview/pages/templates/repo/settings/pipelines.html
+135
appview/pages/templates/repo/settings/pipelines.html
···
···
1
+
{{ define "title" }}{{ .Tab }} settings · {{ .RepoInfo.FullName }}{{ end }}
2
+
3
+
{{ define "repoContent" }}
4
+
<section class="w-full grid grid-cols-1 md:grid-cols-4 gap-2">
5
+
<div class="col-span-1">
6
+
{{ template "repo/settings/fragments/sidebar" . }}
7
+
</div>
8
+
<div class="col-span-1 md:col-span-3 flex flex-col gap-6 p-2">
9
+
{{ template "spindleSettings" . }}
10
+
{{ if $.CurrentSpindle }}
11
+
{{ template "secretSettings" . }}
12
+
{{ end }}
13
+
<div id="operation-error" class="text-red-500 dark:text-red-400"></div>
14
+
</div>
15
+
</section>
16
+
{{ end }}
17
+
18
+
{{ define "spindleSettings" }}
19
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center">
20
+
<div class="col-span-1 md:col-span-2">
21
+
<h2 class="text-sm pb-2 uppercase font-bold">Spindle</h2>
22
+
<p class="text-gray-500 dark:text-gray-400">
23
+
Choose a spindle to execute your workflows on. Spindles can be
24
+
selfhosted,
25
+
<a class="text-gray-500 dark:text-gray-400 underline" href="https://tangled.sh/@tangled.sh/core/blob/master/docs/spindle/hosting.md">
26
+
click to learn more.
27
+
</a>
28
+
</p>
29
+
</div>
30
+
<form hx-post="/{{ $.RepoInfo.FullName }}/settings/spindle" class="col-span-1 md:col-span-1 md:justify-self-end group flex gap-2 items-stretch">
31
+
<select
32
+
id="spindle"
33
+
name="spindle"
34
+
required
35
+
class="p-1 max-w-64 border border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"
36
+
{{ if not $.RepoInfo.Roles.IsOwner }}disabled{{ end }}>
37
+
<option value="" disabled selected >
38
+
Choose a spindle
39
+
</option>
40
+
{{ range $.Spindles }}
41
+
<option value="{{ . }}" class="py-1" {{ if eq . $.CurrentSpindle }}selected{{ end }}>
42
+
{{ . }}
43
+
</option>
44
+
{{ end }}
45
+
</select>
46
+
<button class="btn flex gap-2 items-center" type="submit" {{ if not $.RepoInfo.Roles.IsOwner }}disabled{{ end }}>
47
+
{{ i "check" "size-4" }}
48
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
49
+
</button>
50
+
</form>
51
+
</div>
52
+
{{ end }}
53
+
54
+
{{ define "secretSettings" }}
55
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center">
56
+
<div class="col-span-1 md:col-span-2">
57
+
<h2 class="text-sm pb-2 uppercase font-bold">SECRETS</h2>
58
+
<p class="text-gray-500 dark:text-gray-400">
59
+
Secrets are accessible in workflow runs via environment variables. Anyone
60
+
with collaborator access to this repository can add and use secrets in
61
+
workflow runs.
62
+
</p>
63
+
</div>
64
+
<div class="col-span-1 md:col-span-1 md:justify-self-end">
65
+
{{ template "addSecretButton" . }}
66
+
</div>
67
+
</div>
68
+
<div class="flex flex-col rounded border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700 w-full">
69
+
{{ range .Secrets }}
70
+
{{ template "repo/settings/fragments/secretListing" (list $ .) }}
71
+
{{ else }}
72
+
<div class="flex items-center justify-center p-2 text-gray-500">
73
+
no secrets added yet
74
+
</div>
75
+
{{ end }}
76
+
</div>
77
+
{{ end }}
78
+
79
+
{{ define "addSecretButton" }}
80
+
<button
81
+
class="btn flex items-center gap-2"
82
+
popovertarget="add-secret-modal"
83
+
popovertargetaction="toggle">
84
+
{{ i "plus" "size-4" }}
85
+
add secret
86
+
</button>
87
+
<div
88
+
id="add-secret-modal"
89
+
popover
90
+
class="bg-white w-full md:w-96 dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 drop-shadow dark:text-white backdrop:bg-gray-400/50 dark:backdrop:bg-gray-800/50">
91
+
{{ template "addSecretModal" . }}
92
+
</div>
93
+
{{ end}}
94
+
95
+
{{ define "addSecretModal" }}
96
+
<form
97
+
hx-put="/{{ $.RepoInfo.FullName }}/settings/secrets"
98
+
hx-indicator="#spinner"
99
+
hx-swap="none"
100
+
class="flex flex-col gap-2"
101
+
>
102
+
<p class="uppercase p-0">ADD SECRET</p>
103
+
<p class="text-sm text-gray-500 dark:text-gray-400">Secrets are available as environment variables in the workflow.</p>
104
+
<input
105
+
type="text"
106
+
id="secret-key"
107
+
name="key"
108
+
required
109
+
placeholder="SECRET_NAME"
110
+
/>
111
+
<textarea
112
+
type="text"
113
+
id="secret-value"
114
+
name="value"
115
+
required
116
+
placeholder="secret value"></textarea>
117
+
<div class="flex gap-2 pt-2">
118
+
<button
119
+
type="button"
120
+
popovertarget="add-secret-modal"
121
+
popovertargetaction="hide"
122
+
class="btn w-1/2 flex items-center gap-2 text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
123
+
>
124
+
{{ i "x" "size-4" }} cancel
125
+
</button>
126
+
<button type="submit" class="btn w-1/2 flex items-center">
127
+
<span class="inline-flex gap-2 items-center">{{ i "plus" "size-4" }} add</span>
128
+
<span id="spinner" class="group">
129
+
{{ i "loader-circle" "ml-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
130
+
</span>
131
+
</button>
132
+
</div>
133
+
<div id="add-secret-error" class="text-red-500 dark:text-red-400"></div>
134
+
</form>
135
+
{{ end }}
+248
-98
appview/repo/repo.go
+248
-98
appview/repo/repo.go
···
8
"fmt"
9
"io"
10
"log"
11
"net/http"
12
"net/url"
13
"slices"
···
50
db *db.DB
51
enforcer *rbac.Enforcer
52
notifier notify.Notifier
53
}
54
55
func New(
···
62
config *config.Config,
63
notifier notify.Notifier,
64
enforcer *rbac.Enforcer,
65
) *Repo {
66
return &Repo{oauth: oauth,
67
repoResolver: repoResolver,
···
72
db: db,
73
notifier: notifier,
74
enforcer: enforcer,
75
}
76
}
77
···
588
589
// modify the spindle configured for this repo
590
func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) {
591
f, err := rp.repoResolver.Resolve(r)
592
if err != nil {
593
-
log.Println("failed to get repo and knot", err)
594
-
w.WriteHeader(http.StatusBadRequest)
595
return
596
}
597
598
repoAt := f.RepoAt
599
rkey := repoAt.RecordKey().String()
600
if rkey == "" {
601
-
log.Println("invalid aturi for repo", err)
602
-
w.WriteHeader(http.StatusInternalServerError)
603
return
604
}
605
606
-
user := rp.oauth.GetUser(r)
607
-
608
newSpindle := r.FormValue("spindle")
609
client, err := rp.oauth.AuthorizedClient(r)
610
if err != nil {
611
-
log.Println("failed to get client")
612
-
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
613
return
614
}
615
616
// ensure that this is a valid spindle for this user
617
validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
618
if err != nil {
619
-
log.Println("failed to get valid spindles")
620
-
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
621
return
622
}
623
624
if !slices.Contains(validSpindles, newSpindle) {
625
-
log.Println("newSpindle not present in validSpindles", "newSpindle", newSpindle, "validSpindles", validSpindles)
626
-
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
627
return
628
}
629
630
// optimistic update
631
err = db.UpdateSpindle(rp.db, string(repoAt), newSpindle)
632
if err != nil {
633
-
log.Println("failed to perform update-spindle query", err)
634
-
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
635
return
636
}
637
638
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey)
639
if err != nil {
640
-
// failed to get record
641
-
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, no record found on PDS.")
642
return
643
}
644
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
···
659
})
660
661
if err != nil {
662
-
log.Println("failed to perform update-spindle query", err)
663
-
// failed to get record
664
-
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, unable to save to PDS.")
665
return
666
}
667
···
671
eventconsumer.NewSpindleSource(newSpindle),
672
)
673
674
-
w.Write(fmt.Append(nil, "spindle set to: ", newSpindle))
675
}
676
677
func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) {
678
f, err := rp.repoResolver.Resolve(r)
679
if err != nil {
680
-
log.Println("failed to get repo and knot", err)
681
return
682
}
683
684
collaborator := r.FormValue("collaborator")
685
if collaborator == "" {
686
-
http.Error(w, "malformed form", http.StatusBadRequest)
687
return
688
}
689
690
collaboratorIdent, err := rp.idResolver.ResolveIdent(r.Context(), collaborator)
691
if err != nil {
692
-
w.Write([]byte("failed to resolve collaborator did to a handle"))
693
return
694
}
695
-
log.Printf("adding %s to %s\n", collaboratorIdent.Handle.String(), f.Knot)
696
697
-
// TODO: create an atproto record for this
698
699
secret, err := db.GetRegistrationKey(rp.db, f.Knot)
700
if err != nil {
701
-
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
702
return
703
}
704
705
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev)
706
if err != nil {
707
-
log.Println("failed to create client to ", f.Knot)
708
return
709
}
710
711
ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String())
712
if err != nil {
713
-
log.Printf("failed to make request to %s: %s", f.Knot, err)
714
return
715
}
716
717
if ksResp.StatusCode != http.StatusNoContent {
718
-
w.Write(fmt.Append(nil, "knotserver failed to add collaborator: ", err))
719
return
720
}
721
722
tx, err := rp.db.BeginTx(r.Context(), nil)
723
if err != nil {
724
-
log.Println("failed to start tx")
725
-
w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
726
return
727
}
728
defer func() {
729
tx.Rollback()
730
err = rp.enforcer.E.LoadPolicy()
731
if err != nil {
732
-
log.Println("failed to rollback policies")
733
}
734
}()
735
736
err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo())
737
if err != nil {
738
-
w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
739
return
740
}
741
742
err = db.AddCollaborator(rp.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot)
743
if err != nil {
744
-
w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
745
return
746
}
747
748
err = tx.Commit()
749
if err != nil {
750
-
log.Println("failed to commit changes", err)
751
-
http.Error(w, err.Error(), http.StatusInternalServerError)
752
return
753
}
754
755
err = rp.enforcer.E.SavePolicy()
756
if err != nil {
757
-
log.Println("failed to update ACLs", err)
758
-
http.Error(w, err.Error(), http.StatusInternalServerError)
759
return
760
}
761
762
-
w.Write(fmt.Append(nil, "added collaborator: ", collaboratorIdent.Handle.String()))
763
-
764
}
765
766
func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) {
···
913
}
914
915
func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) {
916
f, err := rp.repoResolver.Resolve(r)
917
if err != nil {
918
log.Println("failed to get repo and knot", err)
···
948
949
switch r.Method {
950
case http.MethodPut:
951
value := r.FormValue("value")
952
-
if key == "" {
953
w.WriteHeader(http.StatusBadRequest)
954
return
955
}
···
964
},
965
)
966
if err != nil {
967
-
log.Println("request didnt run", "err", err)
968
return
969
}
970
971
case http.MethodDelete:
972
err = tangled.RepoRemoveSecret(
973
r.Context(),
974
spindleClient,
···
978
},
979
)
980
if err != nil {
981
-
log.Println("request didnt run", "err", err)
982
return
983
}
984
}
985
}
986
987
func (rp *Repo) RepoSettings(w http.ResponseWriter, r *http.Request) {
988
f, err := rp.repoResolver.Resolve(r)
989
if err != nil {
990
-
log.Println("failed to get repo and knot", err)
991
return
992
}
993
994
-
switch r.Method {
995
-
case http.MethodGet:
996
-
// for now, this is just pubkeys
997
-
user := rp.oauth.GetUser(r)
998
-
repoCollaborators, err := f.Collaborators(r.Context())
999
-
if err != nil {
1000
-
log.Println("failed to get collaborators", err)
1001
-
}
1002
1003
-
isCollaboratorInviteAllowed := false
1004
-
if user != nil {
1005
-
ok, err := rp.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.DidSlashRepo())
1006
-
if err == nil && ok {
1007
-
isCollaboratorInviteAllowed = true
1008
-
}
1009
-
}
1010
1011
-
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
1012
-
if err != nil {
1013
-
log.Println("failed to create unsigned client", err)
1014
-
return
1015
-
}
1016
1017
-
result, err := us.Branches(f.OwnerDid(), f.RepoName)
1018
-
if err != nil {
1019
-
log.Println("failed to reach knotserver", err)
1020
-
return
1021
-
}
1022
1023
-
// all spindles that this user is a member of
1024
-
spindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
1025
-
if err != nil {
1026
-
log.Println("failed to fetch spindles", err)
1027
-
return
1028
-
}
1029
1030
-
var secrets []*tangled.RepoListSecrets_Secret
1031
-
if f.Spindle != "" {
1032
-
if spindleClient, err := rp.oauth.ServiceClient(
1033
-
r,
1034
-
oauth.WithService(f.Spindle),
1035
-
oauth.WithLxm(tangled.RepoListSecretsNSID),
1036
-
oauth.WithDev(rp.config.Core.Dev),
1037
-
); err != nil {
1038
-
log.Println("failed to create spindle client", err)
1039
-
} else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt.String()); err != nil {
1040
-
log.Println("failed to fetch secrets", err)
1041
-
} else {
1042
-
secrets = resp.Secrets
1043
-
}
1044
}
1045
1046
-
rp.pages.RepoSettings(w, pages.RepoSettingsParams{
1047
-
LoggedInUser: user,
1048
-
RepoInfo: f.RepoInfo(user),
1049
-
Collaborators: repoCollaborators,
1050
-
IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,
1051
-
Branches: result.Branches,
1052
-
Spindles: spindles,
1053
-
CurrentSpindle: f.Spindle,
1054
-
Secrets: secrets,
1055
})
1056
}
1057
}
1058
1059
func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) {
···
8
"fmt"
9
"io"
10
"log"
11
+
"log/slog"
12
"net/http"
13
"net/url"
14
"slices"
···
51
db *db.DB
52
enforcer *rbac.Enforcer
53
notifier notify.Notifier
54
+
logger *slog.Logger
55
}
56
57
func New(
···
64
config *config.Config,
65
notifier notify.Notifier,
66
enforcer *rbac.Enforcer,
67
+
logger *slog.Logger,
68
) *Repo {
69
return &Repo{oauth: oauth,
70
repoResolver: repoResolver,
···
75
db: db,
76
notifier: notifier,
77
enforcer: enforcer,
78
+
logger: logger,
79
}
80
}
81
···
592
593
// modify the spindle configured for this repo
594
func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) {
595
+
user := rp.oauth.GetUser(r)
596
+
l := rp.logger.With("handler", "EditSpindle")
597
+
l = l.With("did", user.Did)
598
+
l = l.With("handle", user.Handle)
599
+
600
+
errorId := "operation-error"
601
+
fail := func(msg string, err error) {
602
+
l.Error(msg, "err", err)
603
+
rp.pages.Notice(w, errorId, msg)
604
+
}
605
+
606
f, err := rp.repoResolver.Resolve(r)
607
if err != nil {
608
+
fail("Failed to resolve repo. Try again later", err)
609
return
610
}
611
612
repoAt := f.RepoAt
613
rkey := repoAt.RecordKey().String()
614
if rkey == "" {
615
+
fail("Failed to resolve repo. Try again later", err)
616
return
617
}
618
619
newSpindle := r.FormValue("spindle")
620
client, err := rp.oauth.AuthorizedClient(r)
621
if err != nil {
622
+
fail("Failed to authorize. Try again later.", err)
623
return
624
}
625
626
// ensure that this is a valid spindle for this user
627
validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
628
if err != nil {
629
+
fail("Failed to find spindles. Try again later.", err)
630
return
631
}
632
633
if !slices.Contains(validSpindles, newSpindle) {
634
+
fail("Failed to configure spindle.", fmt.Errorf("%s is not a valid spindle: %q", newSpindle, validSpindles))
635
return
636
}
637
638
// optimistic update
639
err = db.UpdateSpindle(rp.db, string(repoAt), newSpindle)
640
if err != nil {
641
+
fail("Failed to update spindle. Try again later.", err)
642
return
643
}
644
645
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey)
646
if err != nil {
647
+
fail("Failed to update spindle, no record found on PDS.", err)
648
return
649
}
650
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
···
665
})
666
667
if err != nil {
668
+
fail("Failed to update spindle, unable to save to PDS.", err)
669
return
670
}
671
···
675
eventconsumer.NewSpindleSource(newSpindle),
676
)
677
678
+
rp.pages.HxRefresh(w)
679
}
680
681
func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) {
682
+
user := rp.oauth.GetUser(r)
683
+
l := rp.logger.With("handler", "AddCollaborator")
684
+
l = l.With("did", user.Did)
685
+
l = l.With("handle", user.Handle)
686
+
687
f, err := rp.repoResolver.Resolve(r)
688
if err != nil {
689
+
l.Error("failed to get repo and knot", "err", err)
690
return
691
}
692
693
+
errorId := "add-collaborator-error"
694
+
fail := func(msg string, err error) {
695
+
l.Error(msg, "err", err)
696
+
rp.pages.Notice(w, errorId, msg)
697
+
}
698
+
699
collaborator := r.FormValue("collaborator")
700
if collaborator == "" {
701
+
fail("Invalid form.", nil)
702
return
703
}
704
705
collaboratorIdent, err := rp.idResolver.ResolveIdent(r.Context(), collaborator)
706
if err != nil {
707
+
fail(fmt.Sprintf("'%s' is not a valid DID/handle.", collaborator), err)
708
+
return
709
+
}
710
+
711
+
if collaboratorIdent.DID.String() == user.Did {
712
+
fail("You seem to be adding yourself as a collaborator.", nil)
713
return
714
}
715
716
+
l = l.With("collaborator", collaboratorIdent.Handle)
717
+
l = l.With("knot", f.Knot)
718
+
l.Info("adding to knot")
719
720
secret, err := db.GetRegistrationKey(rp.db, f.Knot)
721
if err != nil {
722
+
fail("Failed to add to knot.", err)
723
return
724
}
725
726
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev)
727
if err != nil {
728
+
fail("Failed to add to knot.", err)
729
return
730
}
731
732
ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String())
733
if err != nil {
734
+
fail("Knot was unreachable.", err)
735
return
736
}
737
738
if ksResp.StatusCode != http.StatusNoContent {
739
+
fail(fmt.Sprintf("Knot returned unexpected status code: %d.", ksResp.StatusCode), nil)
740
return
741
}
742
743
tx, err := rp.db.BeginTx(r.Context(), nil)
744
if err != nil {
745
+
fail("Failed to add collaborator.", err)
746
return
747
}
748
defer func() {
749
tx.Rollback()
750
err = rp.enforcer.E.LoadPolicy()
751
if err != nil {
752
+
fail("Failed to add collaborator.", err)
753
}
754
}()
755
756
err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo())
757
if err != nil {
758
+
fail("Failed to add collaborator permissions.", err)
759
return
760
}
761
762
err = db.AddCollaborator(rp.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot)
763
if err != nil {
764
+
fail("Failed to add collaborator.", err)
765
return
766
}
767
768
err = tx.Commit()
769
if err != nil {
770
+
fail("Failed to add collaborator.", err)
771
return
772
}
773
774
err = rp.enforcer.E.SavePolicy()
775
if err != nil {
776
+
fail("Failed to update collaborator permissions.", err)
777
return
778
}
779
780
+
rp.pages.HxRefresh(w)
781
}
782
783
func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) {
···
930
}
931
932
func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) {
933
+
user := rp.oauth.GetUser(r)
934
+
l := rp.logger.With("handler", "Secrets")
935
+
l = l.With("handle", user.Handle)
936
+
l = l.With("did", user.Did)
937
+
938
f, err := rp.repoResolver.Resolve(r)
939
if err != nil {
940
log.Println("failed to get repo and knot", err)
···
970
971
switch r.Method {
972
case http.MethodPut:
973
+
errorId := "add-secret-error"
974
+
975
value := r.FormValue("value")
976
+
if value == "" {
977
w.WriteHeader(http.StatusBadRequest)
978
return
979
}
···
988
},
989
)
990
if err != nil {
991
+
l.Error("Failed to add secret.", "err", err)
992
+
rp.pages.Notice(w, errorId, "Failed to add secret.")
993
return
994
}
995
996
case http.MethodDelete:
997
+
errorId := "operation-error"
998
+
999
err = tangled.RepoRemoveSecret(
1000
r.Context(),
1001
spindleClient,
···
1005
},
1006
)
1007
if err != nil {
1008
+
l.Error("Failed to delete secret.", "err", err)
1009
+
rp.pages.Notice(w, errorId, "Failed to delete secret.")
1010
return
1011
}
1012
}
1013
+
1014
+
rp.pages.HxRefresh(w)
1015
}
1016
1017
+
type tab = map[string]any
1018
+
1019
+
var (
1020
+
// would be great to have ordered maps right about now
1021
+
settingsTabs []tab = []tab{
1022
+
{"Name": "general", "Icon": "sliders-horizontal"},
1023
+
{"Name": "access", "Icon": "users"},
1024
+
{"Name": "pipelines", "Icon": "layers-2"},
1025
+
}
1026
+
)
1027
+
1028
func (rp *Repo) RepoSettings(w http.ResponseWriter, r *http.Request) {
1029
+
tabVal := r.URL.Query().Get("tab")
1030
+
if tabVal == "" {
1031
+
tabVal = "general"
1032
+
}
1033
+
1034
+
switch tabVal {
1035
+
case "general":
1036
+
rp.generalSettings(w, r)
1037
+
1038
+
case "access":
1039
+
rp.accessSettings(w, r)
1040
+
1041
+
case "pipelines":
1042
+
rp.pipelineSettings(w, r)
1043
+
}
1044
+
1045
+
// user := rp.oauth.GetUser(r)
1046
+
// repoCollaborators, err := f.Collaborators(r.Context())
1047
+
// if err != nil {
1048
+
// log.Println("failed to get collaborators", err)
1049
+
// }
1050
+
1051
+
// isCollaboratorInviteAllowed := false
1052
+
// if user != nil {
1053
+
// ok, err := rp.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.DidSlashRepo())
1054
+
// if err == nil && ok {
1055
+
// isCollaboratorInviteAllowed = true
1056
+
// }
1057
+
// }
1058
+
1059
+
// us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
1060
+
// if err != nil {
1061
+
// log.Println("failed to create unsigned client", err)
1062
+
// return
1063
+
// }
1064
+
1065
+
// result, err := us.Branches(f.OwnerDid(), f.RepoName)
1066
+
// if err != nil {
1067
+
// log.Println("failed to reach knotserver", err)
1068
+
// return
1069
+
// }
1070
+
1071
+
// // all spindles that this user is a member of
1072
+
// spindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
1073
+
// if err != nil {
1074
+
// log.Println("failed to fetch spindles", err)
1075
+
// return
1076
+
// }
1077
+
1078
+
// var secrets []*tangled.RepoListSecrets_Secret
1079
+
// if f.Spindle != "" {
1080
+
// if spindleClient, err := rp.oauth.ServiceClient(
1081
+
// r,
1082
+
// oauth.WithService(f.Spindle),
1083
+
// oauth.WithLxm(tangled.RepoListSecretsNSID),
1084
+
// oauth.WithDev(rp.config.Core.Dev),
1085
+
// ); err != nil {
1086
+
// log.Println("failed to create spindle client", err)
1087
+
// } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt.String()); err != nil {
1088
+
// log.Println("failed to fetch secrets", err)
1089
+
// } else {
1090
+
// secrets = resp.Secrets
1091
+
// }
1092
+
// }
1093
+
1094
+
// rp.pages.RepoSettings(w, pages.RepoSettingsParams{
1095
+
// LoggedInUser: user,
1096
+
// RepoInfo: f.RepoInfo(user),
1097
+
// Collaborators: repoCollaborators,
1098
+
// IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,
1099
+
// Branches: result.Branches,
1100
+
// Spindles: spindles,
1101
+
// CurrentSpindle: f.Spindle,
1102
+
// Secrets: secrets,
1103
+
// })
1104
+
}
1105
+
1106
+
func (rp *Repo) generalSettings(w http.ResponseWriter, r *http.Request) {
1107
f, err := rp.repoResolver.Resolve(r)
1108
+
user := rp.oauth.GetUser(r)
1109
+
1110
+
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
1111
if err != nil {
1112
+
log.Println("failed to create unsigned client", err)
1113
return
1114
}
1115
1116
+
result, err := us.Branches(f.OwnerDid(), f.RepoName)
1117
+
if err != nil {
1118
+
log.Println("failed to reach knotserver", err)
1119
+
return
1120
+
}
1121
1122
+
rp.pages.RepoGeneralSettings(w, pages.RepoGeneralSettingsParams{
1123
+
LoggedInUser: user,
1124
+
RepoInfo: f.RepoInfo(user),
1125
+
Branches: result.Branches,
1126
+
Tabs: settingsTabs,
1127
+
Tab: "general",
1128
+
})
1129
+
}
1130
1131
+
func (rp *Repo) accessSettings(w http.ResponseWriter, r *http.Request) {
1132
+
f, err := rp.repoResolver.Resolve(r)
1133
+
user := rp.oauth.GetUser(r)
1134
1135
+
repoCollaborators, err := f.Collaborators(r.Context())
1136
+
if err != nil {
1137
+
log.Println("failed to get collaborators", err)
1138
+
}
1139
1140
+
rp.pages.RepoAccessSettings(w, pages.RepoAccessSettingsParams{
1141
+
LoggedInUser: user,
1142
+
RepoInfo: f.RepoInfo(user),
1143
+
Tabs: settingsTabs,
1144
+
Tab: "access",
1145
+
Collaborators: repoCollaborators,
1146
+
})
1147
+
}
1148
1149
+
func (rp *Repo) pipelineSettings(w http.ResponseWriter, r *http.Request) {
1150
+
f, err := rp.repoResolver.Resolve(r)
1151
+
user := rp.oauth.GetUser(r)
1152
+
1153
+
// all spindles that this user is a member of
1154
+
spindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
1155
+
if err != nil {
1156
+
log.Println("failed to fetch spindles", err)
1157
+
return
1158
+
}
1159
+
1160
+
var secrets []*tangled.RepoListSecrets_Secret
1161
+
if f.Spindle != "" {
1162
+
if spindleClient, err := rp.oauth.ServiceClient(
1163
+
r,
1164
+
oauth.WithService(f.Spindle),
1165
+
oauth.WithLxm(tangled.RepoListSecretsNSID),
1166
+
oauth.WithDev(rp.config.Core.Dev),
1167
+
); err != nil {
1168
+
log.Println("failed to create spindle client", err)
1169
+
} else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt.String()); err != nil {
1170
+
log.Println("failed to fetch secrets", err)
1171
+
} else {
1172
+
secrets = resp.Secrets
1173
}
1174
+
}
1175
+
1176
+
slices.SortFunc(secrets, func(a, b *tangled.RepoListSecrets_Secret) int {
1177
+
return strings.Compare(a.Key, b.Key)
1178
+
})
1179
1180
+
var dids []string
1181
+
for _, s := range secrets {
1182
+
dids = append(dids, s.CreatedBy)
1183
+
}
1184
+
resolvedIdents := rp.idResolver.ResolveIdents(r.Context(), dids)
1185
+
1186
+
// convert to a more manageable form
1187
+
var niceSecret []map[string]any
1188
+
for id, s := range secrets {
1189
+
when, _ := time.Parse(time.RFC3339, s.CreatedAt)
1190
+
niceSecret = append(niceSecret, map[string]any{
1191
+
"Id": id,
1192
+
"Key": s.Key,
1193
+
"CreatedAt": when,
1194
+
"CreatedBy": resolvedIdents[id].Handle.String(),
1195
})
1196
}
1197
+
1198
+
rp.pages.RepoPipelineSettings(w, pages.RepoPipelineSettingsParams{
1199
+
LoggedInUser: user,
1200
+
RepoInfo: f.RepoInfo(user),
1201
+
Tabs: settingsTabs,
1202
+
Tab: "pipelines",
1203
+
Spindles: spindles,
1204
+
CurrentSpindle: f.Spindle,
1205
+
Secrets: niceSecret,
1206
+
})
1207
}
1208
1209
func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) {
+4
-3
appview/reporesolver/resolver.go
+4
-3
appview/reporesolver/resolver.go
+2
-1
appview/state/router.go
+2
-1
appview/state/router.go