appview/pages: rework repo settings page #391

merged
opened by oppi.li targeting master from push-yssxzpkyorwv
Changed files
+803 -264
appview
+44
appview/pages/pages.go
··· 702 702 return p.executeRepo("repo/settings", w, params) 703 703 } 704 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 + 705 749 type RepoIssuesParams struct { 706 750 LoggedInUser *oauth.User 707 751 RepoInfo repoinfo.RepoInfo
+146 -160
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> 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
··· 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 }}
+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 "slices" ··· 50 51 db *db.DB 51 52 enforcer *rbac.Enforcer 52 53 notifier notify.Notifier 54 + logger *slog.Logger 53 55 } 54 56 55 57 func New( ··· 62 64 config *config.Config, 63 65 notifier notify.Notifier, 64 66 enforcer *rbac.Enforcer, 67 + logger *slog.Logger, 65 68 ) *Repo { 66 69 return &Repo{oauth: oauth, 67 70 repoResolver: repoResolver, ··· 72 75 db: db, 73 76 notifier: notifier, 74 77 enforcer: enforcer, 78 + logger: logger, 75 79 } 76 80 } 77 81 ··· 588 592 589 593 // modify the spindle configured for this repo 590 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 + 591 606 f, err := rp.repoResolver.Resolve(r) 592 607 if err != nil { 593 - log.Println("failed to get repo and knot", err) 594 - w.WriteHeader(http.StatusBadRequest) 608 + fail("Failed to resolve repo. Try again later", err) 595 609 return 596 610 } 597 611 598 612 repoAt := f.RepoAt 599 613 rkey := repoAt.RecordKey().String() 600 614 if rkey == "" { 601 - log.Println("invalid aturi for repo", err) 602 - w.WriteHeader(http.StatusInternalServerError) 615 + fail("Failed to resolve repo. Try again later", err) 603 616 return 604 617 } 605 618 606 - user := rp.oauth.GetUser(r) 607 - 608 619 newSpindle := r.FormValue("spindle") 609 620 client, err := rp.oauth.AuthorizedClient(r) 610 621 if err != nil { 611 - log.Println("failed to get client") 612 - rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.") 622 + fail("Failed to authorize. Try again later.", err) 613 623 return 614 624 } 615 625 616 626 // ensure that this is a valid spindle for this user 617 627 validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Did) 618 628 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.") 629 + fail("Failed to find spindles. Try again later.", err) 621 630 return 622 631 } 623 632 624 633 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.") 634 + fail("Failed to configure spindle.", fmt.Errorf("%s is not a valid spindle: %q", newSpindle, validSpindles)) 627 635 return 628 636 } 629 637 630 638 // optimistic update 631 639 err = db.UpdateSpindle(rp.db, string(repoAt), newSpindle) 632 640 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.") 641 + fail("Failed to update spindle. Try again later.", err) 635 642 return 636 643 } 637 644 638 645 ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey) 639 646 if err != nil { 640 - // failed to get record 641 - rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, no record found on PDS.") 647 + fail("Failed to update spindle, no record found on PDS.", err) 642 648 return 643 649 } 644 650 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ ··· 659 665 }) 660 666 661 667 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.") 668 + fail("Failed to update spindle, unable to save to PDS.", err) 665 669 return 666 670 } 667 671 ··· 671 675 eventconsumer.NewSpindleSource(newSpindle), 672 676 ) 673 677 674 - w.Write(fmt.Append(nil, "spindle set to: ", newSpindle)) 678 + rp.pages.HxRefresh(w) 675 679 } 676 680 677 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 + 678 687 f, err := rp.repoResolver.Resolve(r) 679 688 if err != nil { 680 - log.Println("failed to get repo and knot", err) 689 + l.Error("failed to get repo and knot", "err", err) 681 690 return 682 691 } 683 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 + 684 699 collaborator := r.FormValue("collaborator") 685 700 if collaborator == "" { 686 - http.Error(w, "malformed form", http.StatusBadRequest) 701 + fail("Invalid form.", nil) 687 702 return 688 703 } 689 704 690 705 collaboratorIdent, err := rp.idResolver.ResolveIdent(r.Context(), collaborator) 691 706 if err != nil { 692 - w.Write([]byte("failed to resolve collaborator did to a handle")) 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) 693 713 return 694 714 } 695 - log.Printf("adding %s to %s\n", collaboratorIdent.Handle.String(), f.Knot) 696 715 697 - // TODO: create an atproto record for this 716 + l = l.With("collaborator", collaboratorIdent.Handle) 717 + l = l.With("knot", f.Knot) 718 + l.Info("adding to knot") 698 719 699 720 secret, err := db.GetRegistrationKey(rp.db, f.Knot) 700 721 if err != nil { 701 - log.Printf("no key found for domain %s: %s\n", f.Knot, err) 722 + fail("Failed to add to knot.", err) 702 723 return 703 724 } 704 725 705 726 ksClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev) 706 727 if err != nil { 707 - log.Println("failed to create client to ", f.Knot) 728 + fail("Failed to add to knot.", err) 708 729 return 709 730 } 710 731 711 732 ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String()) 712 733 if err != nil { 713 - log.Printf("failed to make request to %s: %s", f.Knot, err) 734 + fail("Knot was unreachable.", err) 714 735 return 715 736 } 716 737 717 738 if ksResp.StatusCode != http.StatusNoContent { 718 - w.Write(fmt.Append(nil, "knotserver failed to add collaborator: ", err)) 739 + fail(fmt.Sprintf("Knot returned unexpected status code: %d.", ksResp.StatusCode), nil) 719 740 return 720 741 } 721 742 722 743 tx, err := rp.db.BeginTx(r.Context(), nil) 723 744 if err != nil { 724 - log.Println("failed to start tx") 725 - w.Write(fmt.Append(nil, "failed to add collaborator: ", err)) 745 + fail("Failed to add collaborator.", err) 726 746 return 727 747 } 728 748 defer func() { 729 749 tx.Rollback() 730 750 err = rp.enforcer.E.LoadPolicy() 731 751 if err != nil { 732 - log.Println("failed to rollback policies") 752 + fail("Failed to add collaborator.", err) 733 753 } 734 754 }() 735 755 736 756 err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo()) 737 757 if err != nil { 738 - w.Write(fmt.Append(nil, "failed to add collaborator: ", err)) 758 + fail("Failed to add collaborator permissions.", err) 739 759 return 740 760 } 741 761 742 762 err = db.AddCollaborator(rp.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot) 743 763 if err != nil { 744 - w.Write(fmt.Append(nil, "failed to add collaborator: ", err)) 764 + fail("Failed to add collaborator.", err) 745 765 return 746 766 } 747 767 748 768 err = tx.Commit() 749 769 if err != nil { 750 - log.Println("failed to commit changes", err) 751 - http.Error(w, err.Error(), http.StatusInternalServerError) 770 + fail("Failed to add collaborator.", err) 752 771 return 753 772 } 754 773 755 774 err = rp.enforcer.E.SavePolicy() 756 775 if err != nil { 757 - log.Println("failed to update ACLs", err) 758 - http.Error(w, err.Error(), http.StatusInternalServerError) 776 + fail("Failed to update collaborator permissions.", err) 759 777 return 760 778 } 761 779 762 - w.Write(fmt.Append(nil, "added collaborator: ", collaboratorIdent.Handle.String())) 763 - 780 + rp.pages.HxRefresh(w) 764 781 } 765 782 766 783 func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) { ··· 913 930 } 914 931 915 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 + 916 938 f, err := rp.repoResolver.Resolve(r) 917 939 if err != nil { 918 940 log.Println("failed to get repo and knot", err) ··· 948 970 949 971 switch r.Method { 950 972 case http.MethodPut: 973 + errorId := "add-secret-error" 974 + 951 975 value := r.FormValue("value") 952 - if key == "" { 976 + if value == "" { 953 977 w.WriteHeader(http.StatusBadRequest) 954 978 return 955 979 } ··· 964 988 }, 965 989 ) 966 990 if err != nil { 967 - log.Println("request didnt run", "err", err) 991 + l.Error("Failed to add secret.", "err", err) 992 + rp.pages.Notice(w, errorId, "Failed to add secret.") 968 993 return 969 994 } 970 995 971 996 case http.MethodDelete: 997 + errorId := "operation-error" 998 + 972 999 err = tangled.RepoRemoveSecret( 973 1000 r.Context(), 974 1001 spindleClient, ··· 978 1005 }, 979 1006 ) 980 1007 if err != nil { 981 - log.Println("request didnt run", "err", err) 1008 + l.Error("Failed to delete secret.", "err", err) 1009 + rp.pages.Notice(w, errorId, "Failed to delete secret.") 982 1010 return 983 1011 } 984 1012 } 1013 + 1014 + rp.pages.HxRefresh(w) 985 1015 } 986 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 + 987 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) { 988 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) 989 1111 if err != nil { 990 - log.Println("failed to get repo and knot", err) 1112 + log.Println("failed to create unsigned client", err) 991 1113 return 992 1114 } 993 1115 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 - } 1116 + result, err := us.Branches(f.OwnerDid(), f.RepoName) 1117 + if err != nil { 1118 + log.Println("failed to reach knotserver", err) 1119 + return 1120 + } 1002 1121 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 - } 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 + } 1010 1130 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 - } 1131 + func (rp *Repo) accessSettings(w http.ResponseWriter, r *http.Request) { 1132 + f, err := rp.repoResolver.Resolve(r) 1133 + user := rp.oauth.GetUser(r) 1016 1134 1017 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 1018 - if err != nil { 1019 - log.Println("failed to reach knotserver", err) 1020 - return 1021 - } 1135 + repoCollaborators, err := f.Collaborators(r.Context()) 1136 + if err != nil { 1137 + log.Println("failed to get collaborators", err) 1138 + } 1022 1139 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 - } 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 + } 1029 1148 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 - } 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 1044 1173 } 1174 + } 1175 + 1176 + slices.SortFunc(secrets, func(a, b *tangled.RepoListSecrets_Secret) int { 1177 + return strings.Compare(a.Key, b.Key) 1178 + }) 1045 1179 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, 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(), 1055 1195 }) 1056 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 + }) 1057 1207 } 1058 1208 1059 1209 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 }