Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

appview/pages: rework repo settings page

Signed-off-by: oppiliappan <me@oppi.li>

authored by

oppiliappan and committed by
Tangled
00354b38 49393144

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