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
···
return p.executeRepo("repo/settings", w, params)
}
type RepoIssuesParams struct {
LoggedInUser *oauth.User
RepoInfo repoinfo.RepoInfo
···
return p.executeRepo("repo/settings", w, params)
}
+
type RepoGeneralSettingsParams struct {
+
LoggedInUser *oauth.User
+
RepoInfo repoinfo.RepoInfo
+
Active string
+
Tabs []map[string]any
+
Tab string
+
Branches []types.Branch
+
}
+
+
func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error {
+
params.Active = "settings"
+
return p.executeRepo("repo/settings/general", w, params)
+
}
+
+
type RepoAccessSettingsParams struct {
+
LoggedInUser *oauth.User
+
RepoInfo repoinfo.RepoInfo
+
Active string
+
Tabs []map[string]any
+
Tab string
+
Collaborators []Collaborator
+
}
+
+
func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error {
+
params.Active = "settings"
+
return p.executeRepo("repo/settings/access", w, params)
+
}
+
+
type RepoPipelineSettingsParams struct {
+
LoggedInUser *oauth.User
+
RepoInfo repoinfo.RepoInfo
+
Active string
+
Tabs []map[string]any
+
Tab string
+
Spindles []string
+
CurrentSpindle string
+
Secrets []map[string]any
+
}
+
+
func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error {
+
params.Active = "settings"
+
return p.executeRepo("repo/settings/pipelines", w, params)
+
}
+
type RepoIssuesParams struct {
LoggedInUser *oauth.User
RepoInfo repoinfo.RepoInfo
+146 -160
appview/pages/templates/repo/settings.html
···
{{ define "title" }}settings · {{ .RepoInfo.FullName }}{{ end }}
{{ define "repoContent" }}
-
<header class="font-bold text-sm mb-4 uppercase dark:text-white">
-
Collaborators
-
</header>
-
<div id="collaborator-list" class="flex flex-col gap-2 mb-2">
-
{{ range .Collaborators }}
-
<div id="collaborator" class="mb-2">
-
<a
-
href="/{{ didOrHandle .Did .Handle }}"
-
class="no-underline hover:underline text-black dark:text-white"
-
>
-
{{ didOrHandle .Did .Handle }}
-
</a>
-
<div>
-
<span class="text-sm text-gray-500 dark:text-gray-400">
-
{{ .Role }}
-
</span>
-
</div>
-
</div>
-
{{ end }}
-
</div>
-
{{ if .RepoInfo.Roles.CollaboratorInviteAllowed }}
-
<form
-
hx-put="/{{ $.RepoInfo.FullName }}/settings/collaborator"
-
class="group"
>
-
<label for="collaborator" class="dark:text-white">
-
add collaborator
-
</label>
-
<input
-
type="text"
-
id="collaborator"
-
name="collaborator"
-
required
-
class="dark:bg-gray-700 dark:text-white"
-
placeholder="enter did or handle"
-
>
-
<button
-
class="btn my-2 flex gap-2 items-center dark:text-white dark:hover:bg-gray-700"
-
type="text"
-
>
-
<span>add</span>
-
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
-
</button>
-
</form>
{{ end }}
<form
-
hx-put="/{{ $.RepoInfo.FullName }}/settings/branches/default"
-
class="mt-6 group"
>
-
<label for="branch">default branch</label>
-
<div class="flex gap-2 items-center">
-
<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">
-
<option
-
value=""
-
disabled
-
selected
-
>
-
Choose a default branch
-
</option>
-
{{ range .Branches }}
-
<option
-
value="{{ .Name }}"
-
class="py-1"
-
{{ if .IsDefault }}
-
selected
-
{{ end }}
-
>
-
{{ .Name }}
-
</option>
-
{{ end }}
-
</select>
-
<button class="btn my-2 flex gap-2 items-center" type="submit">
-
<span>save</span>
-
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
-
</button>
-
</div>
</form>
-
{{ if .RepoInfo.Roles.IsOwner }}
<form
-
hx-post="/{{ $.RepoInfo.FullName }}/settings/spindle"
-
class="mt-6 group"
-
>
-
<label for="spindle">spindle</label>
-
<div class="flex gap-2 items-center">
-
<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">
-
<option
-
value=""
-
selected
-
>
-
None
-
</option>
-
{{ range .Spindles }}
-
<option
-
value="{{ . }}"
-
class="py-1"
-
{{ if eq . $.CurrentSpindle }}
-
selected
-
{{ end }}
-
>
-
{{ . }}
-
</option>
-
{{ end }}
-
</select>
-
<button class="btn my-2 flex gap-2 items-center" type="submit">
-
<span>save</span>
-
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
-
</button>
-
</div>
</form>
-
{{ end }}
-
{{ if $.CurrentSpindle }}
-
<header class="font-bold text-sm mb-4 uppercase dark:text-white">
-
Secrets
-
</header>
-
<div id="secret-list" class="flex flex-col gap-2 mb-2">
-
{{ range $idx, $secret := .Secrets }}
-
{{ with $secret }}
-
<div id="secret-{{$idx}}" class="mb-2">
-
{{ .Key }} created on {{ .CreatedAt }} by {{ .CreatedBy }}
-
</div>
-
{{ end }}
{{ end }}
-
</div>
-
<form
-
hx-put="/{{ $.RepoInfo.FullName }}/settings/secrets"
-
class="mt-6"
-
hx-indicator="#add-secret-spinner">
-
<label for="key">secret key</label>
-
<input
-
type="text"
-
id="key"
-
name="key"
-
required
-
class="dark:bg-gray-700 dark:text-white"
-
placeholder="SECRET_KEY" />
-
<label for="value">secret value</label>
-
<input
-
type="text"
-
id="value"
-
name="value"
-
required
-
class="dark:bg-gray-700 dark:text-white"
-
placeholder="SECRET VALUE" />
-
<button class="btn my-2 flex items-center" type="text">
-
<span>add</span>
-
<span id="add-secret-spinner" class="group">
-
{{ i "loader-circle" "ml-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
-
</span>
-
</button>
-
</form>
-
{{ end }}
-
{{ if .RepoInfo.Roles.RepoDeleteAllowed }}
<form
-
hx-confirm="Are you sure you want to delete this repository?"
-
hx-delete="/{{ $.RepoInfo.FullName }}/settings/delete"
class="mt-6"
-
hx-indicator="#delete-repo-spinner"
-
>
-
<label for="branch">delete repository</label>
-
<button class="btn my-2 flex items-center" type="text">
-
<span>delete</span>
-
<span id="delete-repo-spinner" class="group">
-
{{ i "loader-circle" "ml-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
-
</span>
-
</button>
-
<span>
-
Deleting a repository is irreversible and permanent.
-
</span>
-
</form>
-
{{ end }}
{{ end }}
···
{{ define "title" }}settings &middot; {{ .RepoInfo.FullName }}{{ end }}
+
{{ define "repoContent" }}
+
{{ template "collaboratorSettings" . }}
+
{{ template "branchSettings" . }}
+
{{ template "dangerZone" . }}
+
{{ template "spindleSelector" . }}
+
{{ template "spindleSecrets" . }}
+
{{ end }}
+
{{ define "collaboratorSettings" }}
+
<header class="font-bold text-sm mb-4 uppercase dark:text-white">
+
Collaborators
+
</header>
+
<div id="collaborator-list" class="flex flex-col gap-2 mb-2">
+
{{ range .Collaborators }}
+
<div id="collaborator" class="mb-2">
+
<a
+
href="/{{ didOrHandle .Did .Handle }}"
+
class="no-underline hover:underline text-black dark:text-white"
>
+
{{ didOrHandle .Did .Handle }}
+
</a>
+
<div>
+
<span class="text-sm text-gray-500 dark:text-gray-400">
+
{{ .Role }}
+
</span>
+
</div>
+
</div>
{{ end }}
+
</div>
+
{{ if .RepoInfo.Roles.CollaboratorInviteAllowed }}
<form
+
hx-put="/{{ $.RepoInfo.FullName }}/settings/collaborator"
+
class="group"
>
+
<label for="collaborator" class="dark:text-white">
+
add collaborator
+
</label>
+
<input
+
type="text"
+
id="collaborator"
+
name="collaborator"
+
required
+
class="dark:bg-gray-700 dark:text-white"
+
placeholder="enter did or handle">
+
<button class="btn my-2 flex gap-2 items-center dark:text-white dark:hover:bg-gray-700" type="text">
+
<span>add</span>
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
+
</button>
</form>
+
{{ end }}
+
{{ end }}
+
{{ define "dangerZone" }}
+
{{ if .RepoInfo.Roles.RepoDeleteAllowed }}
<form
+
hx-confirm="Are you sure you want to delete this repository?"
+
hx-delete="/{{ $.RepoInfo.FullName }}/settings/delete"
+
class="mt-6"
+
hx-indicator="#delete-repo-spinner">
+
<label for="branch">delete repository</label>
+
<button class="btn my-2 flex items-center" type="text">
+
<span>delete</span>
+
<span id="delete-repo-spinner" class="group">
+
{{ i "loader-circle" "ml-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
+
</span>
+
</button>
+
<span>
+
Deleting a repository is irreversible and permanent.
+
</span>
</form>
+
{{ end }}
+
{{ end }}
+
{{ define "branchSettings" }}
+
<form hx-put="/{{ $.RepoInfo.FullName }}/settings/branches/default" class="mt-6 group">
+
<label for="branch">default branch</label>
+
<div class="flex gap-2 items-center">
+
<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">
+
<option value="" disabled selected >
+
Choose a default branch
+
</option>
+
{{ range .Branches }}
+
<option value="{{ .Name }}" class="py-1" {{ if .IsDefault }}selected{{ end }} >
+
{{ .Name }}
+
</option>
+
{{ end }}
+
</select>
+
<button class="btn my-2 flex gap-2 items-center" type="submit">
+
<span>save</span>
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
+
</button>
+
</div>
+
</form>
+
{{ end }}
+
{{ define "spindleSelector" }}
+
{{ if .RepoInfo.Roles.IsOwner }}
+
<form hx-post="/{{ $.RepoInfo.FullName }}/settings/spindle" class="mt-6 group" >
+
<label for="spindle">spindle</label>
+
<div class="flex gap-2 items-center">
+
<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">
+
<option value="" selected >
+
None
+
</option>
+
{{ range .Spindles }}
+
<option value="{{ . }}" class="py-1" {{ if eq . $.CurrentSpindle }}selected{{ end }}>
+
{{ . }}
+
</option>
{{ end }}
+
</select>
+
<button class="btn my-2 flex gap-2 items-center" type="submit">
+
<span>save</span>
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
+
</button>
+
</div>
+
</form>
+
{{ end }}
+
{{ end }}
+
{{ define "spindleSecrets" }}
+
{{ if $.CurrentSpindle }}
+
<header class="font-bold text-sm mb-4 uppercase dark:text-white">
+
Secrets
+
</header>
+
<div id="secret-list" class="flex flex-col gap-2 mb-2">
+
{{ range $idx, $secret := .Secrets }}
+
{{ with $secret }}
+
<div id="secret-{{$idx}}" class="mb-2">
+
{{ .Key }} created on {{ .CreatedAt }} by {{ .CreatedBy }}
+
</div>
+
{{ end }}
+
{{ end }}
+
</div>
<form
+
hx-put="/{{ $.RepoInfo.FullName }}/settings/secrets"
class="mt-6"
+
hx-indicator="#add-secret-spinner">
+
<label for="key">secret key</label>
+
<input
+
type="text"
+
id="key"
+
name="key"
+
required
+
class="dark:bg-gray-700 dark:text-white"
+
placeholder="SECRET_KEY" />
+
<label for="value">secret value</label>
+
<input
+
type="text"
+
id="value"
+
name="value"
+
required
+
class="dark:bg-gray-700 dark:text-white"
+
placeholder="SECRET VALUE" />
+
<button class="btn my-2 flex items-center" type="text">
+
<span>add</span>
+
<span id="add-secret-spinner" class="group">
+
{{ i "loader-circle" "ml-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
+
</span>
+
</button>
+
</form>
+
{{ end }}
{{ end }}
+110
appview/pages/templates/repo/settings/access.html
···
···
+
{{ define "title" }}{{ .Tab }} settings &middot; {{ .RepoInfo.FullName }}{{ end }}
+
+
{{ define "repoContent" }}
+
<section class="w-full grid grid-cols-1 md:grid-cols-4 gap-2">
+
<div class="col-span-1">
+
{{ template "repo/settings/fragments/sidebar" . }}
+
</div>
+
<div class="col-span-1 md:col-span-3 flex flex-col gap-6 p-2">
+
{{ template "collaboratorSettings" . }}
+
</div>
+
</section>
+
{{ end }}
+
+
{{ define "collaboratorSettings" }}
+
<div class="grid grid-cols-1 gap-4 items-center">
+
<div class="col-span-1">
+
<h2 class="text-sm pb-2 uppercase font-bold">Collaborators</h2>
+
<p class="text-gray-500 dark:text-gray-400">
+
Any user added as a collaborator will be able to push commits and tags to this repository, upload releases, and workflows.
+
</p>
+
</div>
+
{{ template "collaboratorsGrid" . }}
+
</div>
+
{{ end }}
+
+
{{ define "collaboratorsGrid" }}
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
+
{{ if .RepoInfo.Roles.CollaboratorInviteAllowed }}
+
{{ template "addCollaboratorButton" . }}
+
{{ end }}
+
{{ range .Collaborators }}
+
<div class="border border-gray-200 dark:border-gray-700 rounded p-4">
+
<div class="flex items-center gap-3">
+
<img
+
src="{{ fullAvatar .Handle }}"
+
alt="{{ .Handle }}"
+
class="rounded-full h-10 w-10 border border-gray-300 dark:border-gray-600 flex-shrink-0"/>
+
+
<div class="flex-1 min-w-0">
+
<a href="/{{ .Handle }}" class="block truncate">
+
{{ didOrHandle .Did .Handle }}
+
</a>
+
<p class="text-sm text-gray-500 dark:text-gray-400">{{ .Role }}</p>
+
</div>
+
</div>
+
</div>
+
{{ end }}
+
</div>
+
{{ end }}
+
+
{{ define "addCollaboratorButton" }}
+
<button
+
class="btn block rounded p-4"
+
popovertarget="add-collaborator-modal"
+
popovertargetaction="toggle">
+
<div class="flex items-center gap-3">
+
<div class="w-10 h-10 rounded-full bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
+
{{ i "user-plus" "size-4" }}
+
</div>
+
+
<div class="text-left flex-1 min-w-0 block truncate">
+
Add collaborator
+
</div>
+
</div>
+
</button>
+
<div
+
id="add-collaborator-modal"
+
popover
+
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">
+
{{ template "addCollaboratorModal" . }}
+
</div>
+
{{ end }}
+
+
{{ define "addCollaboratorModal" }}
+
<form
+
hx-put="/{{ $.RepoInfo.FullName }}/settings/collaborator"
+
hx-indicator="#spinner"
+
hx-swap="none"
+
class="flex flex-col gap-2"
+
>
+
<label for="add-collaborator" class="uppercase p-0">
+
ADD COLLABORATOR
+
</label>
+
<p class="text-sm text-gray-500 dark:text-gray-400">Collaborators can push to this repository.</p>
+
<input
+
type="text"
+
id="add-collaborator"
+
name="collaborator"
+
required
+
placeholder="@foo.bsky.social"
+
/>
+
<div class="flex gap-2 pt-2">
+
<button
+
type="button"
+
popovertarget="add-collaborator-modal"
+
popovertargetaction="hide"
+
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"
+
>
+
{{ i "x" "size-4" }} cancel
+
</button>
+
<button type="submit" class="btn w-1/2 flex items-center">
+
<span class="inline-flex gap-2 items-center">{{ i "user-plus" "size-4" }} add</span>
+
<span id="spinner" class="group">
+
{{ i "loader-circle" "ml-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
+
</span>
+
</button>
+
</div>
+
<div id="add-collaborator-error" class="text-red-500 dark:text-red-400"></div>
+
</form>
+
{{ end }}
+29
appview/pages/templates/repo/settings/fragments/secretListing.html
···
···
+
{{ define "repo/settings/fragments/secretListing" }}
+
{{ $root := index . 0 }}
+
{{ $secret := index . 1 }}
+
<div id="secret-{{$secret.Key}}" class="flex items-center justify-between p-2">
+
<div class="hover:no-underline flex flex-col gap-1 text-sm min-w-0 max-w-[80%]">
+
<span class="font-mono">
+
{{ $secret.Key }}
+
</span>
+
<div class="flex flex-wrap text items-center gap-1 text-gray-500 dark:text-gray-400">
+
<span>added by</span>
+
<span>{{ template "user/fragments/picHandleLink" $secret.CreatedBy }}</span>
+
<span class="before:content-['·'] before:select-none"></span>
+
<span>{{ template "repo/fragments/shortTimeAgo" $secret.CreatedAt }}</span>
+
</div>
+
</div>
+
<button
+
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
+
title="Delete secret"
+
hx-delete="/{{ $root.RepoInfo.FullName }}/settings/secrets"
+
hx-swap="none"
+
hx-vals='{"key": "{{ $secret.Key }}"}'
+
hx-confirm="Are you sure you want to delete the secret {{ $secret.Key }}?"
+
>
+
{{ i "trash-2" "w-5 h-5" }}
+
<span class="hidden md:inline">delete</span>
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
+
</button>
+
</div>
+
{{ end }}
+16
appview/pages/templates/repo/settings/fragments/sidebar.html
···
···
+
{{ define "repo/settings/fragments/sidebar" }}
+
{{ $active := .Tab }}
+
{{ $tabs := .Tabs }}
+
<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">
+
{{ $activeTab := "bg-white dark:bg-gray-700 drop-shadow-sm" }}
+
{{ $inactiveTab := "bg-gray-100 dark:bg-gray-800" }}
+
{{ range $tabs }}
+
<a href="/{{ $.RepoInfo.FullName }}/settings?tab={{.Name}}" class="no-underline hover:no-underline hover:bg-gray-100/25 hover:dark:bg-gray-700/25">
+
<div class="flex gap-3 items-center p-2 {{ if eq .Name $active }} {{ $activeTab }} {{ else }} {{ $inactiveTab }} {{ end }}">
+
{{ i .Icon "size-4" }}
+
{{ .Name }}
+
</div>
+
</a>
+
{{ end }}
+
</div>
+
{{ end }}
+68
appview/pages/templates/repo/settings/general.html
···
···
+
{{ define "title" }}{{ .Tab }} settings &middot; {{ .RepoInfo.FullName }}{{ end }}
+
+
{{ define "repoContent" }}
+
<section class="w-full grid grid-cols-1 md:grid-cols-4 gap-2">
+
<div class="col-span-1">
+
{{ template "repo/settings/fragments/sidebar" . }}
+
</div>
+
<div class="col-span-1 md:col-span-3 flex flex-col gap-6 p-2">
+
{{ template "branchSettings" . }}
+
{{ template "deleteRepo" . }}
+
</div>
+
</section>
+
{{ end }}
+
+
{{ define "branchSettings" }}
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center">
+
<div class="col-span-1 md:col-span-2">
+
<h2 class="text-sm pb-2 uppercase font-bold">Default Branch</h2>
+
<p class="text-gray-500 dark:text-gray-400">
+
The default branch is considered the “base” branch in your repository,
+
against which all pull requests and code commits are automatically made,
+
unless you specify a different branch.
+
</p>
+
</div>
+
<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">
+
<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">
+
<option value="" disabled selected >
+
Choose a default branch
+
</option>
+
{{ range .Branches }}
+
<option value="{{ .Name }}" class="py-1" {{ if .IsDefault }}selected{{ end }} >
+
{{ .Name }}
+
</option>
+
{{ end }}
+
</select>
+
<button class="btn flex gap-2 items-center" type="submit">
+
{{ i "check" "size-4" }}
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
+
</button>
+
</form>
+
</div>
+
{{ end }}
+
+
{{ define "deleteRepo" }}
+
{{ if .RepoInfo.Roles.RepoDeleteAllowed }}
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center">
+
<div class="col-span-1 md:col-span-2">
+
<h2 class="text-sm pb-2 uppercase text-red-500 dark:text-red-400 font-bold">Delete Repository</h2>
+
<p class="text-red-500 dark:text-red-400 ">
+
Deleting a repository is irreversible and permanent. Be certain before deleting a repository.
+
</p>
+
</div>
+
<div class="col-span-1 md:col-span-1 md:justify-self-end">
+
<button
+
class="btn group text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 flex gap-2 items-center"
+
type="button"
+
hx-delete="/{{ $.RepoInfo.FullName }}/settings/delete"
+
hx-confirm="Are you sure you want to delete {{ $.RepoInfo.FullName }}?">
+
{{ i "trash-2" "size-4" }}
+
delete
+
<span class="ml-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline">
+
{{ i "loader-circle" "w-4 h-4" }}
+
</span>
+
</button>
+
</div>
+
</div>
+
{{ end }}
+
{{ end }}
+135
appview/pages/templates/repo/settings/pipelines.html
···
···
+
{{ define "title" }}{{ .Tab }} settings &middot; {{ .RepoInfo.FullName }}{{ end }}
+
+
{{ define "repoContent" }}
+
<section class="w-full grid grid-cols-1 md:grid-cols-4 gap-2">
+
<div class="col-span-1">
+
{{ template "repo/settings/fragments/sidebar" . }}
+
</div>
+
<div class="col-span-1 md:col-span-3 flex flex-col gap-6 p-2">
+
{{ template "spindleSettings" . }}
+
{{ if $.CurrentSpindle }}
+
{{ template "secretSettings" . }}
+
{{ end }}
+
<div id="operation-error" class="text-red-500 dark:text-red-400"></div>
+
</div>
+
</section>
+
{{ end }}
+
+
{{ define "spindleSettings" }}
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center">
+
<div class="col-span-1 md:col-span-2">
+
<h2 class="text-sm pb-2 uppercase font-bold">Spindle</h2>
+
<p class="text-gray-500 dark:text-gray-400">
+
Choose a spindle to execute your workflows on. Spindles can be
+
selfhosted,
+
<a class="text-gray-500 dark:text-gray-400 underline" href="https://tangled.sh/@tangled.sh/core/blob/master/docs/spindle/hosting.md">
+
click to learn more.
+
</a>
+
</p>
+
</div>
+
<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">
+
<select
+
id="spindle"
+
name="spindle"
+
required
+
class="p-1 max-w-64 border border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"
+
{{ if not $.RepoInfo.Roles.IsOwner }}disabled{{ end }}>
+
<option value="" disabled selected >
+
Choose a spindle
+
</option>
+
{{ range $.Spindles }}
+
<option value="{{ . }}" class="py-1" {{ if eq . $.CurrentSpindle }}selected{{ end }}>
+
{{ . }}
+
</option>
+
{{ end }}
+
</select>
+
<button class="btn flex gap-2 items-center" type="submit" {{ if not $.RepoInfo.Roles.IsOwner }}disabled{{ end }}>
+
{{ i "check" "size-4" }}
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
+
</button>
+
</form>
+
</div>
+
{{ end }}
+
+
{{ define "secretSettings" }}
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center">
+
<div class="col-span-1 md:col-span-2">
+
<h2 class="text-sm pb-2 uppercase font-bold">SECRETS</h2>
+
<p class="text-gray-500 dark:text-gray-400">
+
Secrets are accessible in workflow runs via environment variables. Anyone
+
with collaborator access to this repository can add and use secrets in
+
workflow runs.
+
</p>
+
</div>
+
<div class="col-span-1 md:col-span-1 md:justify-self-end">
+
{{ template "addSecretButton" . }}
+
</div>
+
</div>
+
<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">
+
{{ range .Secrets }}
+
{{ template "repo/settings/fragments/secretListing" (list $ .) }}
+
{{ else }}
+
<div class="flex items-center justify-center p-2 text-gray-500">
+
no secrets added yet
+
</div>
+
{{ end }}
+
</div>
+
{{ end }}
+
+
{{ define "addSecretButton" }}
+
<button
+
class="btn flex items-center gap-2"
+
popovertarget="add-secret-modal"
+
popovertargetaction="toggle">
+
{{ i "plus" "size-4" }}
+
add secret
+
</button>
+
<div
+
id="add-secret-modal"
+
popover
+
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">
+
{{ template "addSecretModal" . }}
+
</div>
+
{{ end}}
+
+
{{ define "addSecretModal" }}
+
<form
+
hx-put="/{{ $.RepoInfo.FullName }}/settings/secrets"
+
hx-indicator="#spinner"
+
hx-swap="none"
+
class="flex flex-col gap-2"
+
>
+
<p class="uppercase p-0">ADD SECRET</p>
+
<p class="text-sm text-gray-500 dark:text-gray-400">Secrets are available as environment variables in the workflow.</p>
+
<input
+
type="text"
+
id="secret-key"
+
name="key"
+
required
+
placeholder="SECRET_NAME"
+
/>
+
<textarea
+
type="text"
+
id="secret-value"
+
name="value"
+
required
+
placeholder="secret value"></textarea>
+
<div class="flex gap-2 pt-2">
+
<button
+
type="button"
+
popovertarget="add-secret-modal"
+
popovertargetaction="hide"
+
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"
+
>
+
{{ i "x" "size-4" }} cancel
+
</button>
+
<button type="submit" class="btn w-1/2 flex items-center">
+
<span class="inline-flex gap-2 items-center">{{ i "plus" "size-4" }} add</span>
+
<span id="spinner" class="group">
+
{{ i "loader-circle" "ml-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
+
</span>
+
</button>
+
</div>
+
<div id="add-secret-error" class="text-red-500 dark:text-red-400"></div>
+
</form>
+
{{ end }}
+248 -98
appview/repo/repo.go
···
"fmt"
"io"
"log"
"net/http"
"net/url"
"path/filepath"
···
db *db.DB
enforcer *rbac.Enforcer
notifier notify.Notifier
}
func New(
···
config *config.Config,
notifier notify.Notifier,
enforcer *rbac.Enforcer,
) *Repo {
return &Repo{oauth: oauth,
repoResolver: repoResolver,
···
db: db,
notifier: notifier,
enforcer: enforcer,
}
}
···
// modify the spindle configured for this repo
func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) {
f, err := rp.repoResolver.Resolve(r)
if err != nil {
-
log.Println("failed to get repo and knot", err)
-
w.WriteHeader(http.StatusBadRequest)
return
}
repoAt := f.RepoAt
rkey := repoAt.RecordKey().String()
if rkey == "" {
-
log.Println("invalid aturi for repo", err)
-
w.WriteHeader(http.StatusInternalServerError)
return
}
-
user := rp.oauth.GetUser(r)
-
newSpindle := r.FormValue("spindle")
client, err := rp.oauth.AuthorizedClient(r)
if err != nil {
-
log.Println("failed to get client")
-
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
return
}
// ensure that this is a valid spindle for this user
validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
if err != nil {
-
log.Println("failed to get valid spindles")
-
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
return
}
if !slices.Contains(validSpindles, newSpindle) {
-
log.Println("newSpindle not present in validSpindles", "newSpindle", newSpindle, "validSpindles", validSpindles)
-
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
return
}
// optimistic update
err = db.UpdateSpindle(rp.db, string(repoAt), newSpindle)
if err != nil {
-
log.Println("failed to perform update-spindle query", err)
-
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, try again later.")
return
}
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey)
if err != nil {
-
// failed to get record
-
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, no record found on PDS.")
return
}
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
···
})
if err != nil {
-
log.Println("failed to perform update-spindle query", err)
-
// failed to get record
-
rp.pages.Notice(w, "repo-notice", "Failed to configure spindle, unable to save to PDS.")
return
}
···
eventconsumer.NewSpindleSource(newSpindle),
)
-
w.Write(fmt.Append(nil, "spindle set to: ", newSpindle))
}
func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) {
f, err := rp.repoResolver.Resolve(r)
if err != nil {
-
log.Println("failed to get repo and knot", err)
return
}
collaborator := r.FormValue("collaborator")
if collaborator == "" {
-
http.Error(w, "malformed form", http.StatusBadRequest)
return
}
collaboratorIdent, err := rp.idResolver.ResolveIdent(r.Context(), collaborator)
if err != nil {
-
w.Write([]byte("failed to resolve collaborator did to a handle"))
return
}
-
log.Printf("adding %s to %s\n", collaboratorIdent.Handle.String(), f.Knot)
-
// TODO: create an atproto record for this
secret, err := db.GetRegistrationKey(rp.db, f.Knot)
if err != nil {
-
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
return
}
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev)
if err != nil {
-
log.Println("failed to create client to ", f.Knot)
return
}
ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String())
if err != nil {
-
log.Printf("failed to make request to %s: %s", f.Knot, err)
return
}
if ksResp.StatusCode != http.StatusNoContent {
-
w.Write(fmt.Append(nil, "knotserver failed to add collaborator: ", err))
return
}
tx, err := rp.db.BeginTx(r.Context(), nil)
if err != nil {
-
log.Println("failed to start tx")
-
w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
return
}
defer func() {
tx.Rollback()
err = rp.enforcer.E.LoadPolicy()
if err != nil {
-
log.Println("failed to rollback policies")
}
}()
err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo())
if err != nil {
-
w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
return
}
err = db.AddCollaborator(rp.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot)
if err != nil {
-
w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
return
}
err = tx.Commit()
if err != nil {
-
log.Println("failed to commit changes", err)
-
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = rp.enforcer.E.SavePolicy()
if err != nil {
-
log.Println("failed to update ACLs", err)
-
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
-
w.Write(fmt.Append(nil, "added collaborator: ", collaboratorIdent.Handle.String()))
-
}
func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) {
···
}
func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) {
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
···
switch r.Method {
case http.MethodPut:
value := r.FormValue("value")
-
if key == "" {
w.WriteHeader(http.StatusBadRequest)
return
}
···
},
)
if err != nil {
-
log.Println("request didnt run", "err", err)
return
}
case http.MethodDelete:
err = tangled.RepoRemoveSecret(
r.Context(),
spindleClient,
···
},
)
if err != nil {
-
log.Println("request didnt run", "err", err)
return
}
}
}
func (rp *Repo) RepoSettings(w http.ResponseWriter, r *http.Request) {
f, err := rp.repoResolver.Resolve(r)
if err != nil {
-
log.Println("failed to get repo and knot", err)
return
}
-
switch r.Method {
-
case http.MethodGet:
-
// for now, this is just pubkeys
-
user := rp.oauth.GetUser(r)
-
repoCollaborators, err := f.Collaborators(r.Context())
-
if err != nil {
-
log.Println("failed to get collaborators", err)
-
}
-
isCollaboratorInviteAllowed := false
-
if user != nil {
-
ok, err := rp.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.DidSlashRepo())
-
if err == nil && ok {
-
isCollaboratorInviteAllowed = true
-
}
-
}
-
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
-
if err != nil {
-
log.Println("failed to create unsigned client", err)
-
return
-
}
-
result, err := us.Branches(f.OwnerDid(), f.RepoName)
-
if err != nil {
-
log.Println("failed to reach knotserver", err)
-
return
-
}
-
// all spindles that this user is a member of
-
spindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
-
if err != nil {
-
log.Println("failed to fetch spindles", err)
-
return
-
}
-
var secrets []*tangled.RepoListSecrets_Secret
-
if f.Spindle != "" {
-
if spindleClient, err := rp.oauth.ServiceClient(
-
r,
-
oauth.WithService(f.Spindle),
-
oauth.WithLxm(tangled.RepoListSecretsNSID),
-
oauth.WithDev(rp.config.Core.Dev),
-
); err != nil {
-
log.Println("failed to create spindle client", err)
-
} else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt.String()); err != nil {
-
log.Println("failed to fetch secrets", err)
-
} else {
-
secrets = resp.Secrets
-
}
}
-
rp.pages.RepoSettings(w, pages.RepoSettingsParams{
-
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(user),
-
Collaborators: repoCollaborators,
-
IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,
-
Branches: result.Branches,
-
Spindles: spindles,
-
CurrentSpindle: f.Spindle,
-
Secrets: secrets,
})
}
}
func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) {
···
"fmt"
"io"
"log"
+
"log/slog"
"net/http"
"net/url"
"path/filepath"
···
db *db.DB
enforcer *rbac.Enforcer
notifier notify.Notifier
+
logger *slog.Logger
}
func New(
···
config *config.Config,
notifier notify.Notifier,
enforcer *rbac.Enforcer,
+
logger *slog.Logger,
) *Repo {
return &Repo{oauth: oauth,
repoResolver: repoResolver,
···
db: db,
notifier: notifier,
enforcer: enforcer,
+
logger: logger,
}
}
···
// modify the spindle configured for this repo
func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
l := rp.logger.With("handler", "EditSpindle")
+
l = l.With("did", user.Did)
+
l = l.With("handle", user.Handle)
+
+
errorId := "operation-error"
+
fail := func(msg string, err error) {
+
l.Error(msg, "err", err)
+
rp.pages.Notice(w, errorId, msg)
+
}
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
+
fail("Failed to resolve repo. Try again later", err)
return
}
repoAt := f.RepoAt
rkey := repoAt.RecordKey().String()
if rkey == "" {
+
fail("Failed to resolve repo. Try again later", err)
return
}
newSpindle := r.FormValue("spindle")
client, err := rp.oauth.AuthorizedClient(r)
if err != nil {
+
fail("Failed to authorize. Try again later.", err)
return
}
// ensure that this is a valid spindle for this user
validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
if err != nil {
+
fail("Failed to find spindles. Try again later.", err)
return
}
if !slices.Contains(validSpindles, newSpindle) {
+
fail("Failed to configure spindle.", fmt.Errorf("%s is not a valid spindle: %q", newSpindle, validSpindles))
return
}
// optimistic update
err = db.UpdateSpindle(rp.db, string(repoAt), newSpindle)
if err != nil {
+
fail("Failed to update spindle. Try again later.", err)
return
}
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey)
if err != nil {
+
fail("Failed to update spindle, no record found on PDS.", err)
return
}
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
···
})
if err != nil {
+
fail("Failed to update spindle, unable to save to PDS.", err)
return
}
···
eventconsumer.NewSpindleSource(newSpindle),
)
+
rp.pages.HxRefresh(w)
}
func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
l := rp.logger.With("handler", "AddCollaborator")
+
l = l.With("did", user.Did)
+
l = l.With("handle", user.Handle)
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
+
l.Error("failed to get repo and knot", "err", err)
return
}
+
errorId := "add-collaborator-error"
+
fail := func(msg string, err error) {
+
l.Error(msg, "err", err)
+
rp.pages.Notice(w, errorId, msg)
+
}
+
collaborator := r.FormValue("collaborator")
if collaborator == "" {
+
fail("Invalid form.", nil)
return
}
collaboratorIdent, err := rp.idResolver.ResolveIdent(r.Context(), collaborator)
if err != nil {
+
fail(fmt.Sprintf("'%s' is not a valid DID/handle.", collaborator), err)
+
return
+
}
+
+
if collaboratorIdent.DID.String() == user.Did {
+
fail("You seem to be adding yourself as a collaborator.", nil)
return
}
+
l = l.With("collaborator", collaboratorIdent.Handle)
+
l = l.With("knot", f.Knot)
+
l.Info("adding to knot")
secret, err := db.GetRegistrationKey(rp.db, f.Knot)
if err != nil {
+
fail("Failed to add to knot.", err)
return
}
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev)
if err != nil {
+
fail("Failed to add to knot.", err)
return
}
ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String())
if err != nil {
+
fail("Knot was unreachable.", err)
return
}
if ksResp.StatusCode != http.StatusNoContent {
+
fail(fmt.Sprintf("Knot returned unexpected status code: %d.", ksResp.StatusCode), nil)
return
}
tx, err := rp.db.BeginTx(r.Context(), nil)
if err != nil {
+
fail("Failed to add collaborator.", err)
return
}
defer func() {
tx.Rollback()
err = rp.enforcer.E.LoadPolicy()
if err != nil {
+
fail("Failed to add collaborator.", err)
}
}()
err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo())
if err != nil {
+
fail("Failed to add collaborator permissions.", err)
return
}
err = db.AddCollaborator(rp.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot)
if err != nil {
+
fail("Failed to add collaborator.", err)
return
}
err = tx.Commit()
if err != nil {
+
fail("Failed to add collaborator.", err)
return
}
err = rp.enforcer.E.SavePolicy()
if err != nil {
+
fail("Failed to update collaborator permissions.", err)
return
}
+
rp.pages.HxRefresh(w)
}
func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) {
···
}
func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
l := rp.logger.With("handler", "Secrets")
+
l = l.With("handle", user.Handle)
+
l = l.With("did", user.Did)
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
···
switch r.Method {
case http.MethodPut:
+
errorId := "add-secret-error"
+
value := r.FormValue("value")
+
if value == "" {
w.WriteHeader(http.StatusBadRequest)
return
}
···
},
)
if err != nil {
+
l.Error("Failed to add secret.", "err", err)
+
rp.pages.Notice(w, errorId, "Failed to add secret.")
return
}
case http.MethodDelete:
+
errorId := "operation-error"
+
err = tangled.RepoRemoveSecret(
r.Context(),
spindleClient,
···
},
)
if err != nil {
+
l.Error("Failed to delete secret.", "err", err)
+
rp.pages.Notice(w, errorId, "Failed to delete secret.")
return
}
}
+
+
rp.pages.HxRefresh(w)
}
+
type tab = map[string]any
+
+
var (
+
// would be great to have ordered maps right about now
+
settingsTabs []tab = []tab{
+
{"Name": "general", "Icon": "sliders-horizontal"},
+
{"Name": "access", "Icon": "users"},
+
{"Name": "pipelines", "Icon": "layers-2"},
+
}
+
)
+
func (rp *Repo) RepoSettings(w http.ResponseWriter, r *http.Request) {
+
tabVal := r.URL.Query().Get("tab")
+
if tabVal == "" {
+
tabVal = "general"
+
}
+
+
switch tabVal {
+
case "general":
+
rp.generalSettings(w, r)
+
+
case "access":
+
rp.accessSettings(w, r)
+
+
case "pipelines":
+
rp.pipelineSettings(w, r)
+
}
+
+
// user := rp.oauth.GetUser(r)
+
// repoCollaborators, err := f.Collaborators(r.Context())
+
// if err != nil {
+
// log.Println("failed to get collaborators", err)
+
// }
+
+
// isCollaboratorInviteAllowed := false
+
// if user != nil {
+
// ok, err := rp.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.DidSlashRepo())
+
// if err == nil && ok {
+
// isCollaboratorInviteAllowed = true
+
// }
+
// }
+
+
// us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
+
// if err != nil {
+
// log.Println("failed to create unsigned client", err)
+
// return
+
// }
+
+
// result, err := us.Branches(f.OwnerDid(), f.RepoName)
+
// if err != nil {
+
// log.Println("failed to reach knotserver", err)
+
// return
+
// }
+
+
// // all spindles that this user is a member of
+
// spindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
+
// if err != nil {
+
// log.Println("failed to fetch spindles", err)
+
// return
+
// }
+
+
// var secrets []*tangled.RepoListSecrets_Secret
+
// if f.Spindle != "" {
+
// if spindleClient, err := rp.oauth.ServiceClient(
+
// r,
+
// oauth.WithService(f.Spindle),
+
// oauth.WithLxm(tangled.RepoListSecretsNSID),
+
// oauth.WithDev(rp.config.Core.Dev),
+
// ); err != nil {
+
// log.Println("failed to create spindle client", err)
+
// } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt.String()); err != nil {
+
// log.Println("failed to fetch secrets", err)
+
// } else {
+
// secrets = resp.Secrets
+
// }
+
// }
+
+
// rp.pages.RepoSettings(w, pages.RepoSettingsParams{
+
// LoggedInUser: user,
+
// RepoInfo: f.RepoInfo(user),
+
// Collaborators: repoCollaborators,
+
// IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,
+
// Branches: result.Branches,
+
// Spindles: spindles,
+
// CurrentSpindle: f.Spindle,
+
// Secrets: secrets,
+
// })
+
}
+
+
func (rp *Repo) generalSettings(w http.ResponseWriter, r *http.Request) {
f, err := rp.repoResolver.Resolve(r)
+
user := rp.oauth.GetUser(r)
+
+
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
if err != nil {
+
log.Println("failed to create unsigned client", err)
return
}
+
result, err := us.Branches(f.OwnerDid(), f.RepoName)
+
if err != nil {
+
log.Println("failed to reach knotserver", err)
+
return
+
}
+
rp.pages.RepoGeneralSettings(w, pages.RepoGeneralSettingsParams{
+
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(user),
+
Branches: result.Branches,
+
Tabs: settingsTabs,
+
Tab: "general",
+
})
+
}
+
func (rp *Repo) accessSettings(w http.ResponseWriter, r *http.Request) {
+
f, err := rp.repoResolver.Resolve(r)
+
user := rp.oauth.GetUser(r)
+
repoCollaborators, err := f.Collaborators(r.Context())
+
if err != nil {
+
log.Println("failed to get collaborators", err)
+
}
+
rp.pages.RepoAccessSettings(w, pages.RepoAccessSettingsParams{
+
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(user),
+
Tabs: settingsTabs,
+
Tab: "access",
+
Collaborators: repoCollaborators,
+
})
+
}
+
func (rp *Repo) pipelineSettings(w http.ResponseWriter, r *http.Request) {
+
f, err := rp.repoResolver.Resolve(r)
+
user := rp.oauth.GetUser(r)
+
+
// all spindles that this user is a member of
+
spindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
+
if err != nil {
+
log.Println("failed to fetch spindles", err)
+
return
+
}
+
+
var secrets []*tangled.RepoListSecrets_Secret
+
if f.Spindle != "" {
+
if spindleClient, err := rp.oauth.ServiceClient(
+
r,
+
oauth.WithService(f.Spindle),
+
oauth.WithLxm(tangled.RepoListSecretsNSID),
+
oauth.WithDev(rp.config.Core.Dev),
+
); err != nil {
+
log.Println("failed to create spindle client", err)
+
} else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt.String()); err != nil {
+
log.Println("failed to fetch secrets", err)
+
} else {
+
secrets = resp.Secrets
}
+
}
+
+
slices.SortFunc(secrets, func(a, b *tangled.RepoListSecrets_Secret) int {
+
return strings.Compare(a.Key, b.Key)
+
})
+
var dids []string
+
for _, s := range secrets {
+
dids = append(dids, s.CreatedBy)
+
}
+
resolvedIdents := rp.idResolver.ResolveIdents(r.Context(), dids)
+
+
// convert to a more manageable form
+
var niceSecret []map[string]any
+
for id, s := range secrets {
+
when, _ := time.Parse(time.RFC3339, s.CreatedAt)
+
niceSecret = append(niceSecret, map[string]any{
+
"Id": id,
+
"Key": s.Key,
+
"CreatedAt": when,
+
"CreatedBy": resolvedIdents[id].Handle.String(),
})
}
+
+
rp.pages.RepoPipelineSettings(w, pages.RepoPipelineSettingsParams{
+
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(user),
+
Tabs: settingsTabs,
+
Tab: "pipelines",
+
Spindles: spindles,
+
CurrentSpindle: f.Spindle,
+
Secrets: niceSecret,
+
})
}
func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) {
+4 -3
appview/reporesolver/resolver.go
···
for _, item := range repoCollaborators {
// currently only two roles: owner and member
var role string
-
if item[3] == "repo:owner" {
role = "owner"
-
} else if item[3] == "repo:collaborator" {
role = "collaborator"
-
} else {
continue
}
···
for _, item := range repoCollaborators {
// currently only two roles: owner and member
var role string
+
switch item[3] {
+
case "repo:owner":
role = "owner"
+
case "repo:collaborator":
role = "collaborator"
+
default:
continue
}
+2 -1
appview/state/router.go
···
}
func (s *State) RepoRouter(mw *middleware.Middleware) http.Handler {
-
repo := repo.New(s.oauth, s.repoResolver, s.pages, s.spindlestream, s.idResolver, s.db, s.config, s.notifier, s.enforcer)
return repo.Router(mw)
}
···
}
func (s *State) RepoRouter(mw *middleware.Middleware) http.Handler {
+
logger := log.New("repo")
+
repo := repo.New(s.oauth, s.repoResolver, s.pages, s.spindlestream, s.idResolver, s.db, s.config, s.notifier, s.enforcer, logger)
return repo.Router(mw)
}
+1 -2
input.css
···
.prose img {
display: inline;
-
margin-left: 0;
-
margin-right: 0;
vertical-align: middle;
}
}
···
.prose img {
display: inline;
+
margin: 0;
vertical-align: middle;
}
}