Monorepo for Tangled tangled.org

appview/pages: rework repo settings page

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

oppi.li 8e6bb084 49393144

verified
Changed files
+803 -264
appview
+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
··· 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 "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
··· 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 }