···11+{{ define "title" }}{{ .Tab }} settings · {{ .RepoInfo.FullName }}{{ end }}22+33+{{ define "repoContent" }}44+ <section class="w-full grid grid-cols-1 md:grid-cols-4 gap-2">55+ <div class="col-span-1">66+ {{ template "repo/settings/fragments/sidebar" . }}77+ </div>88+ <div class="col-span-1 md:col-span-3 flex flex-col gap-6 p-2">99+ {{ template "spindleSettings" . }}1010+ {{ if $.CurrentSpindle }}1111+ {{ template "secretSettings" . }}1212+ {{ end }}1313+ <div id="operation-error" class="text-red-500 dark:text-red-400"></div>1414+ </div>1515+ </section>1616+{{ end }}1717+1818+{{ define "spindleSettings" }}1919+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center">2020+ <div class="col-span-1 md:col-span-2">2121+ <h2 class="text-sm pb-2 uppercase font-bold">Spindle</h2>2222+ <p class="text-gray-500 dark:text-gray-400">2323+ Choose a spindle to execute your workflows on. Spindles can be2424+ selfhosted,2525+ <a class="text-gray-500 dark:text-gray-400 underline" href="https://tangled.sh/@tangled.sh/core/blob/master/docs/spindle/hosting.md">2626+ click to learn more.2727+ </a>2828+ </p>2929+ </div>3030+ <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">3131+ <select3232+ id="spindle" 3333+ name="spindle"3434+ required 3535+ class="p-1 max-w-64 border border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"3636+ {{ if not $.RepoInfo.Roles.IsOwner }}disabled{{ end }}>3737+ <option value="" disabled selected >3838+ Choose a spindle3939+ </option>4040+ {{ range $.Spindles }}4141+ <option value="{{ . }}" class="py-1" {{ if eq . $.CurrentSpindle }}selected{{ end }}>4242+ {{ . }}4343+ </option>4444+ {{ end }}4545+ </select>4646+ <button class="btn flex gap-2 items-center" type="submit" {{ if not $.RepoInfo.Roles.IsOwner }}disabled{{ end }}>4747+ {{ i "check" "size-4" }}4848+ {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}4949+ </button>5050+ </form>5151+ </div>5252+{{ end }}5353+5454+{{ define "secretSettings" }}5555+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center">5656+ <div class="col-span-1 md:col-span-2">5757+ <h2 class="text-sm pb-2 uppercase font-bold">SECRETS</h2>5858+ <p class="text-gray-500 dark:text-gray-400">5959+ Secrets are accessible in workflow runs via environment variables. Anyone6060+ with collaborator access to this repository can add and use secrets in6161+ workflow runs.6262+ </p>6363+ </div>6464+ <div class="col-span-1 md:col-span-1 md:justify-self-end">6565+ {{ template "addSecretButton" . }}6666+ </div>6767+ </div>6868+ <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">6969+ {{ range .Secrets }}7070+ {{ template "repo/settings/fragments/secretListing" (list $ .) }}7171+ {{ else }}7272+ <div class="flex items-center justify-center p-2 text-gray-500">7373+ no secrets added yet7474+ </div>7575+ {{ end }}7676+ </div>7777+{{ end }}7878+7979+{{ define "addSecretButton" }}8080+ <button 8181+ class="btn flex items-center gap-2"8282+ popovertarget="add-secret-modal"8383+ popovertargetaction="toggle">8484+ {{ i "plus" "size-4" }}8585+ add secret8686+ </button>8787+ <div8888+ id="add-secret-modal"8989+ popover9090+ 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">9191+ {{ template "addSecretModal" . }}9292+ </div>9393+{{ end}}9494+9595+{{ define "addSecretModal" }}9696+<form9797+ hx-put="/{{ $.RepoInfo.FullName }}/settings/secrets"9898+ hx-indicator="#spinner"9999+ hx-swap="none"100100+ class="flex flex-col gap-2"101101+>102102+ <p class="uppercase p-0">ADD SECRET</p>103103+ <p class="text-sm text-gray-500 dark:text-gray-400">Secrets are available as environment variables in the workflow.</p>104104+ <input105105+ type="text"106106+ id="secret-key"107107+ name="key"108108+ required109109+ placeholder="SECRET_NAME"110110+ />111111+ <textarea112112+ type="text"113113+ id="secret-value"114114+ name="value"115115+ required116116+ placeholder="secret value"></textarea>117117+ <div class="flex gap-2 pt-2">118118+ <button119119+ type="button"120120+ popovertarget="add-secret-modal"121121+ popovertargetaction="hide"122122+ 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"123123+ >124124+ {{ i "x" "size-4" }} cancel125125+ </button>126126+ <button type="submit" class="btn w-1/2 flex items-center">127127+ <span class="inline-flex gap-2 items-center">{{ i "plus" "size-4" }} add</span>128128+ <span id="spinner" class="group">129129+ {{ i "loader-circle" "ml-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}130130+ </span>131131+ </button>132132+ </div>133133+ <div id="add-secret-error" class="text-red-500 dark:text-red-400"></div>134134+</form>135135+{{ end }}
+249-99
appview/repo/repo.go
···88 "fmt"99 "io"1010 "log"1111+ "log/slog"1112 "net/http"1213 "net/url"1314 "path/filepath"···5251 db *db.DB5352 enforcer *rbac.Enforcer5453 notifier notify.Notifier5454+ logger *slog.Logger5555}56565757func New(···6563 config *config.Config,6664 notifier notify.Notifier,6765 enforcer *rbac.Enforcer,6666+ logger *slog.Logger,6867) *Repo {6968 return &Repo{oauth: oauth,7069 repoResolver: repoResolver,···7673 db: db,7774 notifier: notifier,7875 enforcer: enforcer,7676+ logger: logger,7977 }8078}8179···631627632628// modify the spindle configured for this repo633629func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) {630630+ user := rp.oauth.GetUser(r)631631+ l := rp.logger.With("handler", "EditSpindle")632632+ l = l.With("did", user.Did)633633+ l = l.With("handle", user.Handle)634634+635635+ errorId := "operation-error"636636+ fail := func(msg string, err error) {637637+ l.Error(msg, "err", err)638638+ rp.pages.Notice(w, errorId, msg)639639+ }640640+634641 f, err := rp.repoResolver.Resolve(r)635642 if err != nil {636636- log.Println("failed to get repo and knot", err)637637- w.WriteHeader(http.StatusBadRequest)643643+ fail("Failed to resolve repo. Try again later", err)638644 return639645 }640646641647 repoAt := f.RepoAt642648 rkey := repoAt.RecordKey().String()643649 if rkey == "" {644644- log.Println("invalid aturi for repo", err)645645- w.WriteHeader(http.StatusInternalServerError)650650+ fail("Failed to resolve repo. Try again later", err)646651 return647652 }648648-649649- user := rp.oauth.GetUser(r)650653651654 newSpindle := r.FormValue("spindle")652655 client, err := rp.oauth.AuthorizedClient(r)653656 if err != nil {654654- log.Println("failed to get client")655655- rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")657657+ fail("Failed to authorize. Try again later.", err)656658 return657659 }658660659661 // ensure that this is a valid spindle for this user660662 validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Did)661663 if err != nil {662662- log.Println("failed to get valid spindles")663663- rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")664664+ fail("Failed to find spindles. Try again later.", err)664665 return665666 }666667667668 if !slices.Contains(validSpindles, newSpindle) {668668- log.Println("newSpindle not present in validSpindles", "newSpindle", newSpindle, "validSpindles", validSpindles)669669- rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")669669+ fail("Failed to configure spindle.", fmt.Errorf("%s is not a valid spindle: %q", newSpindle, validSpindles))670670 return671671 }672672673673 // optimistic update674674 err = db.UpdateSpindle(rp.db, string(repoAt), newSpindle)675675 if err != nil {676676- log.Println("failed to perform update-spindle query", err)677677- rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")676676+ fail("Failed to update spindle. Try again later.", err)678677 return679678 }680679681680 ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey)682681 if err != nil {683683- // failed to get record684684- rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, no record found on PDS.")682682+ fail("Failed to update spindle, no record found on PDS.", err)685683 return686684 }687685 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{···704698 })705699706700 if err != nil {707707- log.Println("failed to perform update-spindle query", err)708708- // failed to get record709709- rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, unable to save to PDS.")701701+ fail("Failed to update spindle, unable to save to PDS.", err)710702 return711703 }712704···714710 eventconsumer.NewSpindleSource(newSpindle),715711 )716712717717- w.Write(fmt.Append(nil, "spindle set to: ", newSpindle))713713+ rp.pages.HxRefresh(w)718714}719715720716func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) {717717+ user := rp.oauth.GetUser(r)718718+ l := rp.logger.With("handler", "AddCollaborator")719719+ l = l.With("did", user.Did)720720+ l = l.With("handle", user.Handle)721721+721722 f, err := rp.repoResolver.Resolve(r)722723 if err != nil {723723- log.Println("failed to get repo and knot", err)724724+ l.Error("failed to get repo and knot", "err", err)724725 return726726+ }727727+728728+ errorId := "add-collaborator-error"729729+ fail := func(msg string, err error) {730730+ l.Error(msg, "err", err)731731+ rp.pages.Notice(w, errorId, msg)725732 }726733727734 collaborator := r.FormValue("collaborator")728735 if collaborator == "" {729729- http.Error(w, "malformed form", http.StatusBadRequest)736736+ fail("Invalid form.", nil)730737 return731738 }732739733740 collaboratorIdent, err := rp.idResolver.ResolveIdent(r.Context(), collaborator)734741 if err != nil {735735- w.Write([]byte("failed to resolve collaborator did to a handle"))742742+ fail(fmt.Sprintf("'%s' is not a valid DID/handle.", collaborator), err)736743 return737744 }738738- log.Printf("adding %s to %s\n", collaboratorIdent.Handle.String(), f.Knot)739745740740- // TODO: create an atproto record for this746746+ if collaboratorIdent.DID.String() == user.Did {747747+ fail("You seem to be adding yourself as a collaborator.", nil)748748+ return749749+ }750750+751751+ l = l.With("collaborator", collaboratorIdent.Handle)752752+ l = l.With("knot", f.Knot)753753+ l.Info("adding to knot")741754742755 secret, err := db.GetRegistrationKey(rp.db, f.Knot)743756 if err != nil {744744- log.Printf("no key found for domain %s: %s\n", f.Knot, err)757757+ fail("Failed to add to knot.", err)745758 return746759 }747760748761 ksClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev)749762 if err != nil {750750- log.Println("failed to create client to ", f.Knot)763763+ fail("Failed to add to knot.", err)751764 return752765 }753766754767 ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String())755768 if err != nil {756756- log.Printf("failed to make request to %s: %s", f.Knot, err)769769+ fail("Knot was unreachable.", err)757770 return758771 }759772760773 if ksResp.StatusCode != http.StatusNoContent {761761- w.Write(fmt.Append(nil, "knotserver failed to add collaborator: ", err))774774+ fail(fmt.Sprintf("Knot returned unexpected status code: %d.", ksResp.StatusCode), nil)762775 return763776 }764777765778 tx, err := rp.db.BeginTx(r.Context(), nil)766779 if err != nil {767767- log.Println("failed to start tx")768768- w.Write(fmt.Append(nil, "failed to add collaborator: ", err))780780+ fail("Failed to add collaborator.", err)769781 return770782 }771783 defer func() {772784 tx.Rollback()773785 err = rp.enforcer.E.LoadPolicy()774786 if err != nil {775775- log.Println("failed to rollback policies")787787+ fail("Failed to add collaborator.", err)776788 }777789 }()778790779791 err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo())780792 if err != nil {781781- w.Write(fmt.Append(nil, "failed to add collaborator: ", err))793793+ fail("Failed to add collaborator permissions.", err)782794 return783795 }784796785797 err = db.AddCollaborator(rp.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot)786798 if err != nil {787787- w.Write(fmt.Append(nil, "failed to add collaborator: ", err))799799+ fail("Failed to add collaborator.", err)788800 return789801 }790802791803 err = tx.Commit()792804 if err != nil {793793- log.Println("failed to commit changes", err)794794- http.Error(w, err.Error(), http.StatusInternalServerError)805805+ fail("Failed to add collaborator.", err)795806 return796807 }797808798809 err = rp.enforcer.E.SavePolicy()799810 if err != nil {800800- log.Println("failed to update ACLs", err)801801- http.Error(w, err.Error(), http.StatusInternalServerError)811811+ fail("Failed to update collaborator permissions.", err)802812 return803813 }804814805805- w.Write(fmt.Append(nil, "added collaborator: ", collaboratorIdent.Handle.String()))806806-815815+ rp.pages.HxRefresh(w)807816}808817809818func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) {···969952}970953971954func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) {955955+ user := rp.oauth.GetUser(r)956956+ l := rp.logger.With("handler", "Secrets")957957+ l = l.With("handle", user.Handle)958958+ l = l.With("did", user.Did)959959+972960 f, err := rp.repoResolver.Resolve(r)973961 if err != nil {974962 log.Println("failed to get repo and knot", err)···10099871010988 switch r.Method {1011989 case http.MethodPut:990990+ errorId := "add-secret-error"991991+1012992 value := r.FormValue("value")10131013- if key == "" {993993+ if value == "" {1014994 w.WriteHeader(http.StatusBadRequest)1015995 return1016996 }···10271003 },10281004 )10291005 if err != nil {10301030- log.Println("request didnt run", "err", err)10061006+ l.Error("Failed to add secret.", "err", err)10071007+ rp.pages.Notice(w, errorId, "Failed to add secret.")10311008 return10321009 }1033101010341011 case http.MethodDelete:10121012+ errorId := "operation-error"10131013+10351014 err = tangled.RepoRemoveSecret(10361015 r.Context(),10371016 spindleClient,···10441017 },10451018 )10461019 if err != nil {10471047- log.Println("request didnt run", "err", err)10201020+ l.Error("Failed to delete secret.", "err", err)10211021+ rp.pages.Notice(w, errorId, "Failed to delete secret.")10481022 return10491023 }10501024 }10251025+10261026+ rp.pages.HxRefresh(w)10511027}1052102810291029+type tab = map[string]any10301030+10311031+var (10321032+ // would be great to have ordered maps right about now10331033+ settingsTabs []tab = []tab{10341034+ {"Name": "general", "Icon": "sliders-horizontal"},10351035+ {"Name": "access", "Icon": "users"},10361036+ {"Name": "pipelines", "Icon": "layers-2"},10371037+ }10381038+)10391039+10531040func (rp *Repo) RepoSettings(w http.ResponseWriter, r *http.Request) {10411041+ tabVal := r.URL.Query().Get("tab")10421042+ if tabVal == "" {10431043+ tabVal = "general"10441044+ }10451045+10461046+ switch tabVal {10471047+ case "general":10481048+ rp.generalSettings(w, r)10491049+10501050+ case "access":10511051+ rp.accessSettings(w, r)10521052+10531053+ case "pipelines":10541054+ rp.pipelineSettings(w, r)10551055+ }10561056+10571057+ // user := rp.oauth.GetUser(r)10581058+ // repoCollaborators, err := f.Collaborators(r.Context())10591059+ // if err != nil {10601060+ // log.Println("failed to get collaborators", err)10611061+ // }10621062+10631063+ // isCollaboratorInviteAllowed := false10641064+ // if user != nil {10651065+ // ok, err := rp.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.DidSlashRepo())10661066+ // if err == nil && ok {10671067+ // isCollaboratorInviteAllowed = true10681068+ // }10691069+ // }10701070+10711071+ // us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)10721072+ // if err != nil {10731073+ // log.Println("failed to create unsigned client", err)10741074+ // return10751075+ // }10761076+10771077+ // result, err := us.Branches(f.OwnerDid(), f.RepoName)10781078+ // if err != nil {10791079+ // log.Println("failed to reach knotserver", err)10801080+ // return10811081+ // }10821082+10831083+ // // all spindles that this user is a member of10841084+ // spindles, err := rp.enforcer.GetSpindlesForUser(user.Did)10851085+ // if err != nil {10861086+ // log.Println("failed to fetch spindles", err)10871087+ // return10881088+ // }10891089+10901090+ // var secrets []*tangled.RepoListSecrets_Secret10911091+ // if f.Spindle != "" {10921092+ // if spindleClient, err := rp.oauth.ServiceClient(10931093+ // r,10941094+ // oauth.WithService(f.Spindle),10951095+ // oauth.WithLxm(tangled.RepoListSecretsNSID),10961096+ // oauth.WithDev(rp.config.Core.Dev),10971097+ // ); err != nil {10981098+ // log.Println("failed to create spindle client", err)10991099+ // } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt.String()); err != nil {11001100+ // log.Println("failed to fetch secrets", err)11011101+ // } else {11021102+ // secrets = resp.Secrets11031103+ // }11041104+ // }11051105+11061106+ // rp.pages.RepoSettings(w, pages.RepoSettingsParams{11071107+ // LoggedInUser: user,11081108+ // RepoInfo: f.RepoInfo(user),11091109+ // Collaborators: repoCollaborators,11101110+ // IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,11111111+ // Branches: result.Branches,11121112+ // Spindles: spindles,11131113+ // CurrentSpindle: f.Spindle,11141114+ // Secrets: secrets,11151115+ // })11161116+}11171117+11181118+func (rp *Repo) generalSettings(w http.ResponseWriter, r *http.Request) {10541119 f, err := rp.repoResolver.Resolve(r)11201120+ user := rp.oauth.GetUser(r)11211121+11221122+ us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)10551123 if err != nil {10561056- log.Println("failed to get repo and knot", err)11241124+ log.Println("failed to create unsigned client", err)10571125 return10581126 }1059112710601060- switch r.Method {10611061- case http.MethodGet:10621062- // for now, this is just pubkeys10631063- user := rp.oauth.GetUser(r)10641064- repoCollaborators, err := f.Collaborators(r.Context())10651065- if err != nil {10661066- log.Println("failed to get collaborators", err)10671067- }11281128+ result, err := us.Branches(f.OwnerDid(), f.RepoName)11291129+ if err != nil {11301130+ log.Println("failed to reach knotserver", err)11311131+ return11321132+ }1068113310691069- isCollaboratorInviteAllowed := false10701070- if user != nil {10711071- ok, err := rp.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.DidSlashRepo())10721072- if err == nil && ok {10731073- isCollaboratorInviteAllowed = true10741074- }10751075- }11341134+ rp.pages.RepoGeneralSettings(w, pages.RepoGeneralSettingsParams{11351135+ LoggedInUser: user,11361136+ RepoInfo: f.RepoInfo(user),11371137+ Branches: result.Branches,11381138+ Tabs: settingsTabs,11391139+ Tab: "general",11401140+ })11411141+}1076114210771077- us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)10781078- if err != nil {10791079- log.Println("failed to create unsigned client", err)10801080- return10811081- }11431143+func (rp *Repo) accessSettings(w http.ResponseWriter, r *http.Request) {11441144+ f, err := rp.repoResolver.Resolve(r)11451145+ user := rp.oauth.GetUser(r)1082114610831083- result, err := us.Branches(f.OwnerDid(), f.RepoName)10841084- if err != nil {10851085- log.Println("failed to reach knotserver", err)10861086- return10871087- }11471147+ repoCollaborators, err := f.Collaborators(r.Context())11481148+ if err != nil {11491149+ log.Println("failed to get collaborators", err)11501150+ }1088115110891089- // all spindles that this user is a member of10901090- spindles, err := rp.enforcer.GetSpindlesForUser(user.Did)10911091- if err != nil {10921092- log.Println("failed to fetch spindles", err)10931093- return10941094- }11521152+ rp.pages.RepoAccessSettings(w, pages.RepoAccessSettingsParams{11531153+ LoggedInUser: user,11541154+ RepoInfo: f.RepoInfo(user),11551155+ Tabs: settingsTabs,11561156+ Tab: "access",11571157+ Collaborators: repoCollaborators,11581158+ })11591159+}1095116010961096- var secrets []*tangled.RepoListSecrets_Secret10971097- if f.Spindle != "" {10981098- if spindleClient, err := rp.oauth.ServiceClient(10991099- r,11001100- oauth.WithService(f.Spindle),11011101- oauth.WithLxm(tangled.RepoListSecretsNSID),11021102- oauth.WithDev(rp.config.Core.Dev),11031103- ); err != nil {11041104- log.Println("failed to create spindle client", err)11051105- } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt.String()); err != nil {11061106- log.Println("failed to fetch secrets", err)11071107- } else {11081108- secrets = resp.Secrets11091109- }11101110- }11611161+func (rp *Repo) pipelineSettings(w http.ResponseWriter, r *http.Request) {11621162+ f, err := rp.repoResolver.Resolve(r)11631163+ user := rp.oauth.GetUser(r)1111116411121112- rp.pages.RepoSettings(w, pages.RepoSettingsParams{11131113- LoggedInUser: user,11141114- RepoInfo: f.RepoInfo(user),11151115- Collaborators: repoCollaborators,11161116- IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,11171117- Branches: result.Branches,11181118- Spindles: spindles,11191119- CurrentSpindle: f.Spindle,11201120- Secrets: secrets,11651165+ // all spindles that this user is a member of11661166+ spindles, err := rp.enforcer.GetSpindlesForUser(user.Did)11671167+ if err != nil {11681168+ log.Println("failed to fetch spindles", err)11691169+ return11701170+ }11711171+11721172+ var secrets []*tangled.RepoListSecrets_Secret11731173+ if f.Spindle != "" {11741174+ if spindleClient, err := rp.oauth.ServiceClient(11751175+ r,11761176+ oauth.WithService(f.Spindle),11771177+ oauth.WithLxm(tangled.RepoListSecretsNSID),11781178+ oauth.WithDev(rp.config.Core.Dev),11791179+ ); err != nil {11801180+ log.Println("failed to create spindle client", err)11811181+ } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt.String()); err != nil {11821182+ log.Println("failed to fetch secrets", err)11831183+ } else {11841184+ secrets = resp.Secrets11851185+ }11861186+ }11871187+11881188+ slices.SortFunc(secrets, func(a, b *tangled.RepoListSecrets_Secret) int {11891189+ return strings.Compare(a.Key, b.Key)11901190+ })11911191+11921192+ var dids []string11931193+ for _, s := range secrets {11941194+ dids = append(dids, s.CreatedBy)11951195+ }11961196+ resolvedIdents := rp.idResolver.ResolveIdents(r.Context(), dids)11971197+11981198+ // convert to a more manageable form11991199+ var niceSecret []map[string]any12001200+ for id, s := range secrets {12011201+ when, _ := time.Parse(time.RFC3339, s.CreatedAt)12021202+ niceSecret = append(niceSecret, map[string]any{12031203+ "Id": id,12041204+ "Key": s.Key,12051205+ "CreatedAt": when,12061206+ "CreatedBy": resolvedIdents[id].Handle.String(),11211207 })11221208 }12091209+12101210+ rp.pages.RepoPipelineSettings(w, pages.RepoPipelineSettingsParams{12111211+ LoggedInUser: user,12121212+ RepoInfo: f.RepoInfo(user),12131213+ Tabs: settingsTabs,12141214+ Tab: "pipelines",12151215+ Spindles: spindles,12161216+ CurrentSpindle: f.Spindle,12171217+ Secrets: niceSecret,12181218+ })11231219}1124122011251221func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) {
+4-3
appview/reporesolver/resolver.go
···149149 for _, item := range repoCollaborators {150150 // currently only two roles: owner and member151151 var role string152152- if item[3] == "repo:owner" {152152+ switch item[3] {153153+ case "repo:owner":153154 role = "owner"154154- } else if item[3] == "repo:collaborator" {155155+ case "repo:collaborator":155156 role = "collaborator"156156- } else {157157+ default:157158 continue158159 }159160