+44
appview/pages/pages.go
+44
appview/pages/pages.go
···
706
706
return p.executeRepo("repo/settings", w, params)
707
707
}
708
708
709
+
type RepoGeneralSettingsParams struct {
710
+
LoggedInUser *oauth.User
711
+
RepoInfo repoinfo.RepoInfo
712
+
Active string
713
+
Tabs []map[string]any
714
+
Tab string
715
+
Branches []types.Branch
716
+
}
717
+
718
+
func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error {
719
+
params.Active = "settings"
720
+
return p.executeRepo("repo/settings/general", w, params)
721
+
}
722
+
723
+
type RepoAccessSettingsParams struct {
724
+
LoggedInUser *oauth.User
725
+
RepoInfo repoinfo.RepoInfo
726
+
Active string
727
+
Tabs []map[string]any
728
+
Tab string
729
+
Collaborators []Collaborator
730
+
}
731
+
732
+
func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error {
733
+
params.Active = "settings"
734
+
return p.executeRepo("repo/settings/access", w, params)
735
+
}
736
+
737
+
type RepoPipelineSettingsParams struct {
738
+
LoggedInUser *oauth.User
739
+
RepoInfo repoinfo.RepoInfo
740
+
Active string
741
+
Tabs []map[string]any
742
+
Tab string
743
+
Spindles []string
744
+
CurrentSpindle string
745
+
Secrets []map[string]any
746
+
}
747
+
748
+
func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error {
749
+
params.Active = "settings"
750
+
return p.executeRepo("repo/settings/pipelines", w, params)
751
+
}
752
+
709
753
type RepoIssuesParams struct {
710
754
LoggedInUser *oauth.User
711
755
RepoInfo repoinfo.RepoInfo
+146
-160
appview/pages/templates/repo/settings.html
+146
-160
appview/pages/templates/repo/settings.html
···
1
1
{{ define "title" }}settings · {{ .RepoInfo.FullName }}{{ end }}
2
+
2
3
{{ define "repoContent" }}
3
-
<header class="font-bold text-sm mb-4 uppercase dark:text-white">
4
-
Collaborators
5
-
</header>
4
+
{{ template "collaboratorSettings" . }}
5
+
{{ template "branchSettings" . }}
6
+
{{ template "dangerZone" . }}
7
+
{{ template "spindleSelector" . }}
8
+
{{ template "spindleSecrets" . }}
9
+
{{ end }}
6
10
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>
11
+
{{ define "collaboratorSettings" }}
12
+
<header class="font-bold text-sm mb-4 uppercase dark:text-white">
13
+
Collaborators
14
+
</header>
24
15
25
-
{{ if .RepoInfo.Roles.CollaboratorInviteAllowed }}
26
-
<form
27
-
hx-put="/{{ $.RepoInfo.FullName }}/settings/collaborator"
28
-
class="group"
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"
29
22
>
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>
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>
49
31
{{ end }}
32
+
</div>
50
33
34
+
{{ if .RepoInfo.Roles.CollaboratorInviteAllowed }}
51
35
<form
52
-
hx-put="/{{ $.RepoInfo.FullName }}/settings/branches/default"
53
-
class="mt-6 group"
36
+
hx-put="/{{ $.RepoInfo.FullName }}/settings/collaborator"
37
+
class="group"
54
38
>
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>
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>
82
53
</form>
54
+
{{ end }}
55
+
{{ end }}
83
56
84
-
{{ if .RepoInfo.Roles.IsOwner }}
57
+
{{ define "dangerZone" }}
58
+
{{ if .RepoInfo.Roles.RepoDeleteAllowed }}
85
59
<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>
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>
115
74
</form>
116
-
{{ end }}
75
+
{{ end }}
76
+
{{ end }}
117
77
118
-
{{ if $.CurrentSpindle }}
119
-
<header class="font-bold text-sm mb-4 uppercase dark:text-white">
120
-
Secrets
121
-
</header>
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 }}
122
99
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 }}
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>
130
113
{{ 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" />
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 }}
152
123
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 }}
124
+
{{ define "spindleSecrets" }}
125
+
{{ if $.CurrentSpindle }}
126
+
<header class="font-bold text-sm mb-4 uppercase dark:text-white">
127
+
Secrets
128
+
</header>
161
129
162
-
{{ if .RepoInfo.Roles.RepoDeleteAllowed }}
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>
163
139
<form
164
-
hx-confirm="Are you sure you want to delete this repository?"
165
-
hx-delete="/{{ $.RepoInfo.FullName }}/settings/delete"
140
+
hx-put="/{{ $.RepoInfo.FullName }}/settings/secrets"
166
141
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 }}
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" />
181
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 }}
182
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
8
"fmt"
9
9
"io"
10
10
"log"
11
+
"log/slog"
11
12
"net/http"
12
13
"net/url"
13
14
"path/filepath"
···
51
52
db *db.DB
52
53
enforcer *rbac.Enforcer
53
54
notifier notify.Notifier
55
+
logger *slog.Logger
54
56
}
55
57
56
58
func New(
···
63
65
config *config.Config,
64
66
notifier notify.Notifier,
65
67
enforcer *rbac.Enforcer,
68
+
logger *slog.Logger,
66
69
) *Repo {
67
70
return &Repo{oauth: oauth,
68
71
repoResolver: repoResolver,
···
73
76
db: db,
74
77
notifier: notifier,
75
78
enforcer: enforcer,
79
+
logger: logger,
76
80
}
77
81
}
78
82
···
627
631
628
632
// modify the spindle configured for this repo
629
633
func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) {
634
+
user := rp.oauth.GetUser(r)
635
+
l := rp.logger.With("handler", "EditSpindle")
636
+
l = l.With("did", user.Did)
637
+
l = l.With("handle", user.Handle)
638
+
639
+
errorId := "operation-error"
640
+
fail := func(msg string, err error) {
641
+
l.Error(msg, "err", err)
642
+
rp.pages.Notice(w, errorId, msg)
643
+
}
644
+
630
645
f, err := rp.repoResolver.Resolve(r)
631
646
if err != nil {
632
-
log.Println("failed to get repo and knot", err)
633
-
w.WriteHeader(http.StatusBadRequest)
647
+
fail("Failed to resolve repo. Try again later", err)
634
648
return
635
649
}
636
650
637
651
repoAt := f.RepoAt
638
652
rkey := repoAt.RecordKey().String()
639
653
if rkey == "" {
640
-
log.Println("invalid aturi for repo", err)
641
-
w.WriteHeader(http.StatusInternalServerError)
654
+
fail("Failed to resolve repo. Try again later", err)
642
655
return
643
656
}
644
657
645
-
user := rp.oauth.GetUser(r)
646
-
647
658
newSpindle := r.FormValue("spindle")
648
659
client, err := rp.oauth.AuthorizedClient(r)
649
660
if err != nil {
650
-
log.Println("failed to get client")
651
-
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
661
+
fail("Failed to authorize. Try again later.", err)
652
662
return
653
663
}
654
664
655
665
// ensure that this is a valid spindle for this user
656
666
validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
657
667
if err != nil {
658
-
log.Println("failed to get valid spindles")
659
-
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
668
+
fail("Failed to find spindles. Try again later.", err)
660
669
return
661
670
}
662
671
663
672
if !slices.Contains(validSpindles, newSpindle) {
664
-
log.Println("newSpindle not present in validSpindles", "newSpindle", newSpindle, "validSpindles", validSpindles)
665
-
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
673
+
fail("Failed to configure spindle.", fmt.Errorf("%s is not a valid spindle: %q", newSpindle, validSpindles))
666
674
return
667
675
}
668
676
669
677
// optimistic update
670
678
err = db.UpdateSpindle(rp.db, string(repoAt), newSpindle)
671
679
if err != nil {
672
-
log.Println("failed to perform update-spindle query", err)
673
-
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
680
+
fail("Failed to update spindle. Try again later.", err)
674
681
return
675
682
}
676
683
677
684
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey)
678
685
if err != nil {
679
-
// failed to get record
680
-
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, no record found on PDS.")
686
+
fail("Failed to update spindle, no record found on PDS.", err)
681
687
return
682
688
}
683
689
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
···
698
704
})
699
705
700
706
if err != nil {
701
-
log.Println("failed to perform update-spindle query", err)
702
-
// failed to get record
703
-
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, unable to save to PDS.")
707
+
fail("Failed to update spindle, unable to save to PDS.", err)
704
708
return
705
709
}
706
710
···
710
714
eventconsumer.NewSpindleSource(newSpindle),
711
715
)
712
716
713
-
w.Write(fmt.Append(nil, "spindle set to: ", newSpindle))
717
+
rp.pages.HxRefresh(w)
714
718
}
715
719
716
720
func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) {
721
+
user := rp.oauth.GetUser(r)
722
+
l := rp.logger.With("handler", "AddCollaborator")
723
+
l = l.With("did", user.Did)
724
+
l = l.With("handle", user.Handle)
725
+
717
726
f, err := rp.repoResolver.Resolve(r)
718
727
if err != nil {
719
-
log.Println("failed to get repo and knot", err)
728
+
l.Error("failed to get repo and knot", "err", err)
720
729
return
721
730
}
722
731
732
+
errorId := "add-collaborator-error"
733
+
fail := func(msg string, err error) {
734
+
l.Error(msg, "err", err)
735
+
rp.pages.Notice(w, errorId, msg)
736
+
}
737
+
723
738
collaborator := r.FormValue("collaborator")
724
739
if collaborator == "" {
725
-
http.Error(w, "malformed form", http.StatusBadRequest)
740
+
fail("Invalid form.", nil)
726
741
return
727
742
}
728
743
729
744
collaboratorIdent, err := rp.idResolver.ResolveIdent(r.Context(), collaborator)
730
745
if err != nil {
731
-
w.Write([]byte("failed to resolve collaborator did to a handle"))
746
+
fail(fmt.Sprintf("'%s' is not a valid DID/handle.", collaborator), err)
732
747
return
733
748
}
734
-
log.Printf("adding %s to %s\n", collaboratorIdent.Handle.String(), f.Knot)
735
749
736
-
// TODO: create an atproto record for this
750
+
if collaboratorIdent.DID.String() == user.Did {
751
+
fail("You seem to be adding yourself as a collaborator.", nil)
752
+
return
753
+
}
754
+
755
+
l = l.With("collaborator", collaboratorIdent.Handle)
756
+
l = l.With("knot", f.Knot)
757
+
l.Info("adding to knot")
737
758
738
759
secret, err := db.GetRegistrationKey(rp.db, f.Knot)
739
760
if err != nil {
740
-
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
761
+
fail("Failed to add to knot.", err)
741
762
return
742
763
}
743
764
744
765
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev)
745
766
if err != nil {
746
-
log.Println("failed to create client to ", f.Knot)
767
+
fail("Failed to add to knot.", err)
747
768
return
748
769
}
749
770
750
771
ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String())
751
772
if err != nil {
752
-
log.Printf("failed to make request to %s: %s", f.Knot, err)
773
+
fail("Knot was unreachable.", err)
753
774
return
754
775
}
755
776
756
777
if ksResp.StatusCode != http.StatusNoContent {
757
-
w.Write(fmt.Append(nil, "knotserver failed to add collaborator: ", err))
778
+
fail(fmt.Sprintf("Knot returned unexpected status code: %d.", ksResp.StatusCode), nil)
758
779
return
759
780
}
760
781
761
782
tx, err := rp.db.BeginTx(r.Context(), nil)
762
783
if err != nil {
763
-
log.Println("failed to start tx")
764
-
w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
784
+
fail("Failed to add collaborator.", err)
765
785
return
766
786
}
767
787
defer func() {
768
788
tx.Rollback()
769
789
err = rp.enforcer.E.LoadPolicy()
770
790
if err != nil {
771
-
log.Println("failed to rollback policies")
791
+
fail("Failed to add collaborator.", err)
772
792
}
773
793
}()
774
794
775
795
err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo())
776
796
if err != nil {
777
-
w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
797
+
fail("Failed to add collaborator permissions.", err)
778
798
return
779
799
}
780
800
781
801
err = db.AddCollaborator(rp.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot)
782
802
if err != nil {
783
-
w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
803
+
fail("Failed to add collaborator.", err)
784
804
return
785
805
}
786
806
787
807
err = tx.Commit()
788
808
if err != nil {
789
-
log.Println("failed to commit changes", err)
790
-
http.Error(w, err.Error(), http.StatusInternalServerError)
809
+
fail("Failed to add collaborator.", err)
791
810
return
792
811
}
793
812
794
813
err = rp.enforcer.E.SavePolicy()
795
814
if err != nil {
796
-
log.Println("failed to update ACLs", err)
797
-
http.Error(w, err.Error(), http.StatusInternalServerError)
815
+
fail("Failed to update collaborator permissions.", err)
798
816
return
799
817
}
800
818
801
-
w.Write(fmt.Append(nil, "added collaborator: ", collaboratorIdent.Handle.String()))
802
-
819
+
rp.pages.HxRefresh(w)
803
820
}
804
821
805
822
func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) {
···
952
969
}
953
970
954
971
func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) {
972
+
user := rp.oauth.GetUser(r)
973
+
l := rp.logger.With("handler", "Secrets")
974
+
l = l.With("handle", user.Handle)
975
+
l = l.With("did", user.Did)
976
+
955
977
f, err := rp.repoResolver.Resolve(r)
956
978
if err != nil {
957
979
log.Println("failed to get repo and knot", err)
···
987
1009
988
1010
switch r.Method {
989
1011
case http.MethodPut:
1012
+
errorId := "add-secret-error"
1013
+
990
1014
value := r.FormValue("value")
991
-
if key == "" {
1015
+
if value == "" {
992
1016
w.WriteHeader(http.StatusBadRequest)
993
1017
return
994
1018
}
···
1003
1027
},
1004
1028
)
1005
1029
if err != nil {
1006
-
log.Println("request didnt run", "err", err)
1030
+
l.Error("Failed to add secret.", "err", err)
1031
+
rp.pages.Notice(w, errorId, "Failed to add secret.")
1007
1032
return
1008
1033
}
1009
1034
1010
1035
case http.MethodDelete:
1036
+
errorId := "operation-error"
1037
+
1011
1038
err = tangled.RepoRemoveSecret(
1012
1039
r.Context(),
1013
1040
spindleClient,
···
1017
1044
},
1018
1045
)
1019
1046
if err != nil {
1020
-
log.Println("request didnt run", "err", err)
1047
+
l.Error("Failed to delete secret.", "err", err)
1048
+
rp.pages.Notice(w, errorId, "Failed to delete secret.")
1021
1049
return
1022
1050
}
1023
1051
}
1052
+
1053
+
rp.pages.HxRefresh(w)
1024
1054
}
1025
1055
1056
+
type tab = map[string]any
1057
+
1058
+
var (
1059
+
// would be great to have ordered maps right about now
1060
+
settingsTabs []tab = []tab{
1061
+
{"Name": "general", "Icon": "sliders-horizontal"},
1062
+
{"Name": "access", "Icon": "users"},
1063
+
{"Name": "pipelines", "Icon": "layers-2"},
1064
+
}
1065
+
)
1066
+
1026
1067
func (rp *Repo) RepoSettings(w http.ResponseWriter, r *http.Request) {
1068
+
tabVal := r.URL.Query().Get("tab")
1069
+
if tabVal == "" {
1070
+
tabVal = "general"
1071
+
}
1072
+
1073
+
switch tabVal {
1074
+
case "general":
1075
+
rp.generalSettings(w, r)
1076
+
1077
+
case "access":
1078
+
rp.accessSettings(w, r)
1079
+
1080
+
case "pipelines":
1081
+
rp.pipelineSettings(w, r)
1082
+
}
1083
+
1084
+
// user := rp.oauth.GetUser(r)
1085
+
// repoCollaborators, err := f.Collaborators(r.Context())
1086
+
// if err != nil {
1087
+
// log.Println("failed to get collaborators", err)
1088
+
// }
1089
+
1090
+
// isCollaboratorInviteAllowed := false
1091
+
// if user != nil {
1092
+
// ok, err := rp.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.DidSlashRepo())
1093
+
// if err == nil && ok {
1094
+
// isCollaboratorInviteAllowed = true
1095
+
// }
1096
+
// }
1097
+
1098
+
// us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
1099
+
// if err != nil {
1100
+
// log.Println("failed to create unsigned client", err)
1101
+
// return
1102
+
// }
1103
+
1104
+
// result, err := us.Branches(f.OwnerDid(), f.RepoName)
1105
+
// if err != nil {
1106
+
// log.Println("failed to reach knotserver", err)
1107
+
// return
1108
+
// }
1109
+
1110
+
// // all spindles that this user is a member of
1111
+
// spindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
1112
+
// if err != nil {
1113
+
// log.Println("failed to fetch spindles", err)
1114
+
// return
1115
+
// }
1116
+
1117
+
// var secrets []*tangled.RepoListSecrets_Secret
1118
+
// if f.Spindle != "" {
1119
+
// if spindleClient, err := rp.oauth.ServiceClient(
1120
+
// r,
1121
+
// oauth.WithService(f.Spindle),
1122
+
// oauth.WithLxm(tangled.RepoListSecretsNSID),
1123
+
// oauth.WithDev(rp.config.Core.Dev),
1124
+
// ); err != nil {
1125
+
// log.Println("failed to create spindle client", err)
1126
+
// } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt.String()); err != nil {
1127
+
// log.Println("failed to fetch secrets", err)
1128
+
// } else {
1129
+
// secrets = resp.Secrets
1130
+
// }
1131
+
// }
1132
+
1133
+
// rp.pages.RepoSettings(w, pages.RepoSettingsParams{
1134
+
// LoggedInUser: user,
1135
+
// RepoInfo: f.RepoInfo(user),
1136
+
// Collaborators: repoCollaborators,
1137
+
// IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,
1138
+
// Branches: result.Branches,
1139
+
// Spindles: spindles,
1140
+
// CurrentSpindle: f.Spindle,
1141
+
// Secrets: secrets,
1142
+
// })
1143
+
}
1144
+
1145
+
func (rp *Repo) generalSettings(w http.ResponseWriter, r *http.Request) {
1027
1146
f, err := rp.repoResolver.Resolve(r)
1147
+
user := rp.oauth.GetUser(r)
1148
+
1149
+
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
1028
1150
if err != nil {
1029
-
log.Println("failed to get repo and knot", err)
1151
+
log.Println("failed to create unsigned client", err)
1152
+
return
1153
+
}
1154
+
1155
+
result, err := us.Branches(f.OwnerDid(), f.RepoName)
1156
+
if err != nil {
1157
+
log.Println("failed to reach knotserver", err)
1030
1158
return
1031
1159
}
1032
1160
1033
-
switch r.Method {
1034
-
case http.MethodGet:
1035
-
// for now, this is just pubkeys
1036
-
user := rp.oauth.GetUser(r)
1037
-
repoCollaborators, err := f.Collaborators(r.Context())
1038
-
if err != nil {
1039
-
log.Println("failed to get collaborators", err)
1040
-
}
1161
+
rp.pages.RepoGeneralSettings(w, pages.RepoGeneralSettingsParams{
1162
+
LoggedInUser: user,
1163
+
RepoInfo: f.RepoInfo(user),
1164
+
Branches: result.Branches,
1165
+
Tabs: settingsTabs,
1166
+
Tab: "general",
1167
+
})
1168
+
}
1169
+
1170
+
func (rp *Repo) accessSettings(w http.ResponseWriter, r *http.Request) {
1171
+
f, err := rp.repoResolver.Resolve(r)
1172
+
user := rp.oauth.GetUser(r)
1173
+
1174
+
repoCollaborators, err := f.Collaborators(r.Context())
1175
+
if err != nil {
1176
+
log.Println("failed to get collaborators", err)
1177
+
}
1178
+
1179
+
rp.pages.RepoAccessSettings(w, pages.RepoAccessSettingsParams{
1180
+
LoggedInUser: user,
1181
+
RepoInfo: f.RepoInfo(user),
1182
+
Tabs: settingsTabs,
1183
+
Tab: "access",
1184
+
Collaborators: repoCollaborators,
1185
+
})
1186
+
}
1041
1187
1042
-
isCollaboratorInviteAllowed := false
1043
-
if user != nil {
1044
-
ok, err := rp.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.DidSlashRepo())
1045
-
if err == nil && ok {
1046
-
isCollaboratorInviteAllowed = true
1047
-
}
1048
-
}
1188
+
func (rp *Repo) pipelineSettings(w http.ResponseWriter, r *http.Request) {
1189
+
f, err := rp.repoResolver.Resolve(r)
1190
+
user := rp.oauth.GetUser(r)
1049
1191
1050
-
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
1051
-
if err != nil {
1052
-
log.Println("failed to create unsigned client", err)
1053
-
return
1054
-
}
1192
+
// all spindles that this user is a member of
1193
+
spindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
1194
+
if err != nil {
1195
+
log.Println("failed to fetch spindles", err)
1196
+
return
1197
+
}
1055
1198
1056
-
result, err := us.Branches(f.OwnerDid(), f.RepoName)
1057
-
if err != nil {
1058
-
log.Println("failed to reach knotserver", err)
1059
-
return
1199
+
var secrets []*tangled.RepoListSecrets_Secret
1200
+
if f.Spindle != "" {
1201
+
if spindleClient, err := rp.oauth.ServiceClient(
1202
+
r,
1203
+
oauth.WithService(f.Spindle),
1204
+
oauth.WithLxm(tangled.RepoListSecretsNSID),
1205
+
oauth.WithDev(rp.config.Core.Dev),
1206
+
); err != nil {
1207
+
log.Println("failed to create spindle client", err)
1208
+
} else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt.String()); err != nil {
1209
+
log.Println("failed to fetch secrets", err)
1210
+
} else {
1211
+
secrets = resp.Secrets
1060
1212
}
1213
+
}
1061
1214
1062
-
// all spindles that this user is a member of
1063
-
spindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
1064
-
if err != nil {
1065
-
log.Println("failed to fetch spindles", err)
1066
-
return
1067
-
}
1215
+
slices.SortFunc(secrets, func(a, b *tangled.RepoListSecrets_Secret) int {
1216
+
return strings.Compare(a.Key, b.Key)
1217
+
})
1068
1218
1069
-
var secrets []*tangled.RepoListSecrets_Secret
1070
-
if f.Spindle != "" {
1071
-
if spindleClient, err := rp.oauth.ServiceClient(
1072
-
r,
1073
-
oauth.WithService(f.Spindle),
1074
-
oauth.WithLxm(tangled.RepoListSecretsNSID),
1075
-
oauth.WithDev(rp.config.Core.Dev),
1076
-
); err != nil {
1077
-
log.Println("failed to create spindle client", err)
1078
-
} else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt.String()); err != nil {
1079
-
log.Println("failed to fetch secrets", err)
1080
-
} else {
1081
-
secrets = resp.Secrets
1082
-
}
1083
-
}
1219
+
var dids []string
1220
+
for _, s := range secrets {
1221
+
dids = append(dids, s.CreatedBy)
1222
+
}
1223
+
resolvedIdents := rp.idResolver.ResolveIdents(r.Context(), dids)
1084
1224
1085
-
rp.pages.RepoSettings(w, pages.RepoSettingsParams{
1086
-
LoggedInUser: user,
1087
-
RepoInfo: f.RepoInfo(user),
1088
-
Collaborators: repoCollaborators,
1089
-
IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,
1090
-
Branches: result.Branches,
1091
-
Spindles: spindles,
1092
-
CurrentSpindle: f.Spindle,
1093
-
Secrets: secrets,
1225
+
// convert to a more manageable form
1226
+
var niceSecret []map[string]any
1227
+
for id, s := range secrets {
1228
+
when, _ := time.Parse(time.RFC3339, s.CreatedAt)
1229
+
niceSecret = append(niceSecret, map[string]any{
1230
+
"Id": id,
1231
+
"Key": s.Key,
1232
+
"CreatedAt": when,
1233
+
"CreatedBy": resolvedIdents[id].Handle.String(),
1094
1234
})
1095
1235
}
1236
+
1237
+
rp.pages.RepoPipelineSettings(w, pages.RepoPipelineSettingsParams{
1238
+
LoggedInUser: user,
1239
+
RepoInfo: f.RepoInfo(user),
1240
+
Tabs: settingsTabs,
1241
+
Tab: "pipelines",
1242
+
Spindles: spindles,
1243
+
CurrentSpindle: f.Spindle,
1244
+
Secrets: niceSecret,
1245
+
})
1096
1246
}
1097
1247
1098
1248
func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) {
+4
-3
appview/reporesolver/resolver.go
+4
-3
appview/reporesolver/resolver.go
···
149
149
for _, item := range repoCollaborators {
150
150
// currently only two roles: owner and member
151
151
var role string
152
-
if item[3] == "repo:owner" {
152
+
switch item[3] {
153
+
case "repo:owner":
153
154
role = "owner"
154
-
} else if item[3] == "repo:collaborator" {
155
+
case "repo:collaborator":
155
156
role = "collaborator"
156
-
} else {
157
+
default:
157
158
continue
158
159
}
159
160
+2
-1
appview/state/router.go
+2
-1
appview/state/router.go
···
208
208
}
209
209
210
210
func (s *State) RepoRouter(mw *middleware.Middleware) http.Handler {
211
-
repo := repo.New(s.oauth, s.repoResolver, s.pages, s.spindlestream, s.idResolver, s.db, s.config, s.notifier, s.enforcer)
211
+
logger := log.New("repo")
212
+
repo := repo.New(s.oauth, s.repoResolver, s.pages, s.spindlestream, s.idResolver, s.db, s.config, s.notifier, s.enforcer, logger)
212
213
return repo.Router(mw)
213
214
}
214
215