forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

appview/state: Moved /knots & /spindle to /settings

Signed-off-by: 5jiji <git@5jiji.com>
Signed-off-by: Seongmin Lee <git@boltless.me>

Changed files
+246 -106
appview
docs
+18 -1
appview/knots/knots.go
···
Knotstream *eventconsumer.Consumer
}
func (k *Knots) Router() http.Handler {
r := chi.NewRouter()
···
k.Pages.Knots(w, pages.KnotsParams{
LoggedInUser: user,
Registrations: registrations,
})
}
···
Members: members,
Repos: repoMap,
IsOwner: true,
})
}
···
}
// success
-
k.Pages.HxRedirect(w, fmt.Sprintf("/knots/%s", domain))
}
func (k *Knots) removeMember(w http.ResponseWriter, r *http.Request) {
···
Knotstream *eventconsumer.Consumer
}
+
type tab = map[string]any
+
+
var (
+
knotsTabs []tab = []tab{
+
{"Name": "profile", "Icon": "user"},
+
{"Name": "keys", "Icon": "key"},
+
{"Name": "emails", "Icon": "mail"},
+
{"Name": "notifications", "Icon": "bell"},
+
{"Name": "knots", "Icon": "volleyball"},
+
{"Name": "spindles", "Icon": "spool"},
+
}
+
)
+
func (k *Knots) Router() http.Handler {
r := chi.NewRouter()
···
k.Pages.Knots(w, pages.KnotsParams{
LoggedInUser: user,
Registrations: registrations,
+
Tabs: knotsTabs,
+
Tab: "knots",
})
}
···
Members: members,
Repos: repoMap,
IsOwner: true,
+
Tabs: knotsTabs,
+
Tab: "knots",
})
}
···
}
// success
+
k.Pages.HxRedirect(w, fmt.Sprintf("/settings/knots/%s", domain))
}
func (k *Knots) removeMember(w http.ResponseWriter, r *http.Request) {
+10
appview/pages/pages.go
···
type KnotsParams struct {
LoggedInUser *oauth.User
Registrations []models.Registration
}
func (p *Pages) Knots(w io.Writer, params KnotsParams) error {
···
Members []string
Repos map[string][]models.Repo
IsOwner bool
}
func (p *Pages) Knot(w io.Writer, params KnotParams) error {
···
type SpindlesParams struct {
LoggedInUser *oauth.User
Spindles []models.Spindle
}
func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error {
···
type SpindleListingParams struct {
models.Spindle
}
func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error {
···
Spindle models.Spindle
Members []string
Repos map[string][]models.Repo
}
func (p *Pages) SpindleDashboard(w io.Writer, params SpindleDashboardParams) error {
···
type KnotsParams struct {
LoggedInUser *oauth.User
Registrations []models.Registration
+
Tabs []map[string]any
+
Tab string
}
func (p *Pages) Knots(w io.Writer, params KnotsParams) error {
···
Members []string
Repos map[string][]models.Repo
IsOwner bool
+
Tabs []map[string]any
+
Tab string
}
func (p *Pages) Knot(w io.Writer, params KnotParams) error {
···
type SpindlesParams struct {
LoggedInUser *oauth.User
Spindles []models.Spindle
+
Tabs []map[string]any
+
Tab string
}
func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error {
···
type SpindleListingParams struct {
models.Spindle
+
Tabs []map[string]any
+
Tab string
}
func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error {
···
Spindle models.Spindle
Members []string
Repos map[string][]models.Repo
+
Tabs []map[string]any
+
Tab string
}
func (p *Pages) SpindleDashboard(w io.Writer, params SpindleDashboardParams) error {
+23 -7
appview/pages/templates/knots/dashboard.html
···
-
{{ define "title" }}{{ .Registration.Domain }} &middot; knots{{ end }}
{{ define "content" }}
-
<div class="px-6 py-4">
<div class="flex justify-between items-center">
-
<h1 class="text-xl font-bold dark:text-white">{{ .Registration.Domain }}</h1>
<div id="right-side" class="flex gap-2">
{{ $style := "px-2 py-1 rounded flex items-center flex-shrink-0 gap-2" }}
{{ $isOwner := and .LoggedInUser (eq .LoggedInUser.Did .Registration.ByDid) }}
···
</div>
{{ if .Members }}
-
<section class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
<div class="flex flex-col gap-2">
{{ block "member" . }} {{ end }}
</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 knot"
-
hx-delete="/knots/{{ .Domain }}"
hx-swap="outerHTML"
hx-confirm="Are you sure you want to delete the knot '{{ .Domain }}'?"
hx-headers='{"shouldRedirect": "true"}'
···
<button
class="btn gap-2 group"
title="Retry knot verification"
-
hx-post="/knots/{{ .Domain }}/retry"
hx-swap="none"
hx-headers='{"shouldRefresh": "true"}'
>
···
<button
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
title="Remove member"
-
hx-post="/knots/{{ $root.Registration.Domain }}/remove"
hx-swap="none"
hx-vals='{"member": "{{$member}}" }'
hx-confirm="Are you sure you want to remove {{ $memberHandle }} from this knot?"
···
+
{{ define "title" }}{{ .Registration.Domain }} &middot; {{ .Tab }} settings{{ end }}
{{ define "content" }}
+
<div class="p-6">
+
<p class="text-xl font-bold dark:text-white">Settings</p>
+
</div>
+
<div class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
+
<section class="w-full grid grid-cols-1 md:grid-cols-4 gap-6">
+
<div class="col-span-1">
+
{{ template "user/settings/fragments/sidebar" . }}
+
</div>
+
<div class="col-span-1 md:col-span-3 flex flex-col gap-6">
+
{{ template "knotDash" . }}
+
</div>
+
</section>
+
</div>
+
{{ end }}
+
+
{{ define "knotDash" }}
+
<div>
<div class="flex justify-between items-center">
+
<h2 class="text-sm pb-2 uppercase font-bold">{{ .Tab }} &middot; {{ .Registration.Domain }}</h2>
<div id="right-side" class="flex gap-2">
{{ $style := "px-2 py-1 rounded flex items-center flex-shrink-0 gap-2" }}
{{ $isOwner := and .LoggedInUser (eq .LoggedInUser.Did .Registration.ByDid) }}
···
</div>
{{ if .Members }}
+
<section class="bg-white dark:bg-gray-800 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
<div class="flex flex-col gap-2">
{{ block "member" . }} {{ end }}
</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 knot"
+
hx-delete="/settings/knots/{{ .Domain }}"
hx-swap="outerHTML"
hx-confirm="Are you sure you want to delete the knot '{{ .Domain }}'?"
hx-headers='{"shouldRedirect": "true"}'
···
<button
class="btn gap-2 group"
title="Retry knot verification"
+
hx-post="/settings/knots/{{ .Domain }}/retry"
hx-swap="none"
hx-headers='{"shouldRefresh": "true"}'
>
···
<button
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
title="Remove member"
+
hx-post="/settings/knots/{{ $root.Registration.Domain }}/remove"
hx-swap="none"
hx-vals='{"member": "{{$member}}" }'
hx-confirm="Are you sure you want to remove {{ $memberHandle }} from this knot?"
+1 -1
appview/pages/templates/knots/fragments/addMemberModal.html
···
{{ define "addKnotMemberPopover" }}
<form
-
hx-post="/knots/{{ .Domain }}/add"
hx-indicator="#spinner"
hx-swap="none"
class="flex flex-col gap-2"
···
{{ define "addKnotMemberPopover" }}
<form
+
hx-post="/settings/knots/{{ .Domain }}/add"
hx-indicator="#spinner"
hx-swap="none"
class="flex flex-col gap-2"
+3 -3
appview/pages/templates/knots/fragments/knotListing.html
···
{{ define "knotLeftSide" }}
{{ if .Registered }}
-
<a href="/knots/{{ .Domain }}" class="hover:no-underline flex items-center gap-2 min-w-0 max-w-[60%]">
{{ i "hard-drive" "w-4 h-4" }}
<span class="hover:underline">
{{ .Domain }}
···
<button
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
title="Delete knot"
-
hx-delete="/knots/{{ .Domain }}"
hx-swap="outerHTML"
hx-target="#knot-{{.Id}}"
hx-confirm="Are you sure you want to delete the knot '{{ .Domain }}'?"
···
<button
class="btn gap-2 group"
title="Retry knot verification"
-
hx-post="/knots/{{ .Domain }}/retry"
hx-swap="none"
hx-target="#knot-{{.Id}}"
>
···
{{ define "knotLeftSide" }}
{{ if .Registered }}
+
<a href="/settings/knots/{{ .Domain }}" class="hover:no-underline flex items-center gap-2 min-w-0 max-w-[60%]">
{{ i "hard-drive" "w-4 h-4" }}
<span class="hover:underline">
{{ .Domain }}
···
<button
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
title="Delete knot"
+
hx-delete="/settings/knots/{{ .Domain }}"
hx-swap="outerHTML"
hx-target="#knot-{{.Id}}"
hx-confirm="Are you sure you want to delete the knot '{{ .Domain }}'?"
···
<button
class="btn gap-2 group"
title="Retry knot verification"
+
hx-post="/settings/knots/{{ .Domain }}/retry"
hx-swap="none"
hx-target="#knot-{{.Id}}"
>
+42 -11
appview/pages/templates/knots/index.html
···
-
{{ define "title" }}knots{{ end }}
{{ define "content" }}
-
<div class="px-6 py-4 flex items-center justify-between gap-4 align-bottom">
-
<h1 class="text-xl font-bold dark:text-white">Knots</h1>
-
<span class="flex items-center gap-1">
-
{{ i "book" "w-3 h-3" }}
-
<a href="https://tangled.org/@tangled.org/core/blob/master/docs/knot-hosting.md">docs</a>
-
</span>
-
</div>
-
<section class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
<div class="flex flex-col gap-6">
-
{{ block "about" . }} {{ end }}
{{ block "list" . }} {{ end }}
{{ block "register" . }} {{ end }}
</div>
···
<h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">register a knot</h2>
<p class="mb-2 dark:text-gray-300">Enter the hostname of your knot to get started.</p>
<form
-
hx-post="/knots/register"
class="max-w-2xl mb-2 space-y-4"
hx-indicator="#register-button"
hx-swap="none"
···
</section>
{{ end }}
···
+
{{ define "title" }}{{ .Tab }} settings{{ end }}
{{ define "content" }}
+
<div class="p-6">
+
<p class="text-xl font-bold dark:text-white">Settings</p>
+
</div>
+
<div class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
+
<section class="w-full grid grid-cols-1 md:grid-cols-4 gap-6">
+
<div class="col-span-1">
+
{{ template "user/settings/fragments/sidebar" . }}
+
</div>
+
<div class="col-span-1 md:col-span-3 flex flex-col gap-6">
+
{{ template "knotsList" . }}
+
</div>
+
</section>
+
</div>
+
{{ end }}
+
+
{{ define "knotsList" }}
+
<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">Knots</h2>
+
{{ block "about" . }} {{ end }}
+
</div>
+
<div class="col-span-1 md:col-span-1 md:justify-self-end">
+
{{ template "docsButton" . }}
+
</div>
+
</div>
+
<section>
<div class="flex flex-col gap-6">
{{ block "list" . }} {{ end }}
{{ block "register" . }} {{ end }}
</div>
···
<h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">register a knot</h2>
<p class="mb-2 dark:text-gray-300">Enter the hostname of your knot to get started.</p>
<form
+
hx-post="/settings/knots/register"
class="max-w-2xl mb-2 space-y-4"
hx-indicator="#register-button"
hx-swap="none"
···
</section>
{{ end }}
+
+
{{ define "docsButton" }}
+
<a
+
class="btn flex items-center gap-2"
+
href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/hosting.md">
+
{{ i "book" "size-4" }}
+
docs
+
</a>
+
<div
+
id="add-email-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">
+
</div>
+
{{ end }}
-2
appview/pages/templates/layouts/fragments/topbar.html
···
<a href="/{{ $user }}">profile</a>
<a href="/{{ $user }}?tab=repos">repositories</a>
<a href="/{{ $user }}?tab=strings">strings</a>
-
<a href="/knots">knots</a>
-
<a href="/spindles">spindles</a>
<a href="/settings">settings</a>
<a href="#"
hx-post="/logout"
···
<a href="/{{ $user }}">profile</a>
<a href="/{{ $user }}?tab=repos">repositories</a>
<a href="/{{ $user }}?tab=strings">strings</a>
<a href="/settings">settings</a>
<a href="#"
hx-post="/logout"
+1 -1
appview/pages/templates/repo/fork.html
···
{{ end }}
</div>
</div>
-
<p class="text-sm text-gray-500 dark:text-gray-400">A knot hosts repository data. <a href="/knots" class="underline">Learn how to register your own knot.</a></p>
</fieldset>
<div class="space-y-2">
···
{{ end }}
</div>
</div>
+
<p class="text-sm text-gray-500 dark:text-gray-400">A knot hosts repository data. <a href="/settings/knots" class="underline">Learn how to register your own knot.</a></p>
</fieldset>
<div class="space-y-2">
+1 -1
appview/pages/templates/repo/new.html
···
</div>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
A knot hosts repository data and handles Git operations.
-
You can also <a href="/knots" class="underline">register your own knot</a>.
</p>
</div>
{{ end }}
···
</div>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
A knot hosts repository data and handles Git operations.
+
You can also <a href="/settings/knots" class="underline">register your own knot</a>.
</p>
</div>
{{ end }}
+22 -6
appview/pages/templates/spindles/dashboard.html
···
-
{{ define "title" }}{{.Spindle.Instance}} &middot; spindles{{ end }}
{{ define "content" }}
-
<div class="px-6 py-4">
<div class="flex justify-between items-center">
-
<h1 class="text-xl font-bold dark:text-white">{{ .Spindle.Instance }}</h1>
<div id="right-side" class="flex gap-2">
{{ $style := "px-2 py-1 rounded flex items-center flex-shrink-0 gap-2" }}
{{ $isOwner := and .LoggedInUser (eq .LoggedInUser.Did .Spindle.Owner) }}
···
<button
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
title="Delete spindle"
-
hx-delete="/spindles/{{ .Instance }}"
hx-swap="outerHTML"
hx-confirm="Are you sure you want to delete the spindle '{{ .Instance }}'?"
hx-headers='{"shouldRedirect": "true"}'
···
<button
class="btn gap-2 group"
title="Retry spindle verification"
-
hx-post="/spindles/{{ .Instance }}/retry"
hx-swap="none"
hx-headers='{"shouldRefresh": "true"}'
>
···
<button
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
title="Remove member"
-
hx-post="/spindles/{{ $root.Spindle.Instance }}/remove"
hx-swap="none"
hx-vals='{"member": "{{$member}}" }'
hx-confirm="Are you sure you want to remove {{ resolve $member }} from this instance?"
···
+
{{ define "title" }}{{.Spindle.Instance}} &middot; {{ .Tab }} settings{{ end }}
{{ define "content" }}
+
<div class="p-6">
+
<p class="text-xl font-bold dark:text-white">Settings</p>
+
</div>
+
<div class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
+
<section class="w-full grid grid-cols-1 md:grid-cols-4 gap-6">
+
<div class="col-span-1">
+
{{ template "user/settings/fragments/sidebar" . }}
+
</div>
+
<div class="col-span-1 md:col-span-3 flex flex-col gap-6">
+
{{ template "spindleDash" . }}
+
</div>
+
</section>
+
</div>
+
{{ end }}
+
+
{{ define "spindleDash" }}
+
<div>
<div class="flex justify-between items-center">
+
<h2 class="text-sm pb-2 uppercase font-bold">{{ .Tab }} &middot; {{ .Spindle.Instance }}</h2>
<div id="right-side" class="flex gap-2">
{{ $style := "px-2 py-1 rounded flex items-center flex-shrink-0 gap-2" }}
{{ $isOwner := and .LoggedInUser (eq .LoggedInUser.Did .Spindle.Owner) }}
···
<button
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
title="Delete spindle"
+
hx-delete="/settings/spindles/{{ .Instance }}"
hx-swap="outerHTML"
hx-confirm="Are you sure you want to delete the spindle '{{ .Instance }}'?"
hx-headers='{"shouldRedirect": "true"}'
···
<button
class="btn gap-2 group"
title="Retry spindle verification"
+
hx-post="/settings/spindles/{{ .Instance }}/retry"
hx-swap="none"
hx-headers='{"shouldRefresh": "true"}'
>
···
<button
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
title="Remove member"
+
hx-post="/settings/spindles/{{ $root.Spindle.Instance }}/remove"
hx-swap="none"
hx-vals='{"member": "{{$member}}" }'
hx-confirm="Are you sure you want to remove {{ resolve $member }} from this instance?"
+1 -1
appview/pages/templates/spindles/fragments/addMemberModal.html
···
{{ define "addSpindleMemberPopover" }}
<form
-
hx-post="/spindles/{{ .Instance }}/add"
hx-indicator="#spinner"
hx-swap="none"
class="flex flex-col gap-2"
···
{{ define "addSpindleMemberPopover" }}
<form
+
hx-post="/settings/spindles/{{ .Instance }}/add"
hx-indicator="#spinner"
hx-swap="none"
class="flex flex-col gap-2"
+3 -3
appview/pages/templates/spindles/fragments/spindleListing.html
···
{{ define "spindleLeftSide" }}
{{ if .Verified }}
-
<a href="/spindles/{{ .Instance }}" class="hover:no-underline flex items-center gap-2 min-w-0 max-w-[60%]">
{{ i "hard-drive" "w-4 h-4" }}
<span class="hover:underline">
{{ .Instance }}
···
<button
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
title="Delete spindle"
-
hx-delete="/spindles/{{ .Instance }}"
hx-swap="outerHTML"
hx-target="#spindle-{{.Id}}"
hx-confirm="Are you sure you want to delete the spindle '{{ .Instance }}'?"
···
<button
class="btn gap-2 group"
title="Retry spindle verification"
-
hx-post="/spindles/{{ .Instance }}/retry"
hx-swap="none"
hx-target="#spindle-{{.Id}}"
>
···
{{ define "spindleLeftSide" }}
{{ if .Verified }}
+
<a href="/settings/spindles/{{ .Instance }}" class="hover:no-underline flex items-center gap-2 min-w-0 max-w-[60%]">
{{ i "hard-drive" "w-4 h-4" }}
<span class="hover:underline">
{{ .Instance }}
···
<button
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
title="Delete spindle"
+
hx-delete="/settings/spindles/{{ .Instance }}"
hx-swap="outerHTML"
hx-target="#spindle-{{.Id}}"
hx-confirm="Are you sure you want to delete the spindle '{{ .Instance }}'?"
···
<button
class="btn gap-2 group"
title="Retry spindle verification"
+
hx-post="/settings/spindles/{{ .Instance }}/retry"
hx-swap="none"
hx-target="#spindle-{{.Id}}"
>
+90 -59
appview/pages/templates/spindles/index.html
···
-
{{ define "title" }}spindles{{ end }}
{{ define "content" }}
-
<div class="px-6 py-4 flex items-center justify-between gap-4 align-bottom">
-
<h1 class="text-xl font-bold dark:text-white">Spindles</h1>
-
<span class="flex items-center gap-1">
-
{{ i "book" "w-3 h-3" }}
-
<a href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/hosting.md">docs</a>
-
</span>
</div>
-
<section class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
<div class="flex flex-col gap-6">
-
{{ block "about" . }} {{ end }}
{{ block "list" . }} {{ end }}
{{ block "register" . }} {{ end }}
</div>
···
{{ define "about" }}
<section class="rounded flex items-center gap-2">
-
<p class="text-gray-500 dark:text-gray-400">
-
Spindles are small CI runners.
-
</p>
</section>
{{ end }}
{{ define "list" }}
-
<section class="rounded w-full flex flex-col gap-2">
-
<h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">your spindles</h2>
-
<div class="flex flex-col rounded border border-gray-200 dark:border-gray-700 w-full">
-
{{ range $spindle := .Spindles }}
-
{{ template "spindles/fragments/spindleListing" . }}
-
{{ else }}
-
<div class="flex items-center justify-center p-2 border-b border-gray-200 dark:border-gray-700 text-gray-500">
-
no spindles registered yet
-
</div>
-
{{ end }}
</div>
-
<div id="operation-error" class="text-red-500 dark:text-red-400"></div>
-
</section>
{{ end }}
{{ define "register" }}
-
<section class="rounded w-full lg:w-fit flex flex-col gap-2">
-
<h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">register a spindle</h2>
-
<p class="mb-2 dark:text-gray-300">Enter the hostname of your spindle to get started.</p>
-
<form
-
hx-post="/spindles/register"
-
class="max-w-2xl mb-2 space-y-4"
-
hx-indicator="#register-button"
-
hx-swap="none"
-
>
-
<div class="flex gap-2">
-
<input
-
type="text"
-
id="instance"
-
name="instance"
-
placeholder="spindle.example.com"
-
required
-
class="flex-1 w-full dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400 px-3 py-2 border rounded"
-
>
-
<button
-
type="submit"
-
id="register-button"
-
class="btn rounded flex items-center py-2 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 group"
-
>
-
<span class="inline-flex items-center gap-2">
-
{{ i "plus" "w-4 h-4" }}
-
register
-
</span>
-
<span class="pl-2 hidden group-[.htmx-request]:inline">
-
{{ i "loader-circle" "w-4 h-4 animate-spin" }}
-
</span>
-
</button>
-
</div>
-
<div id="register-error" class="dark:text-red-400"></div>
-
</form>
-
</section>
{{ end }}
···
+
{{ define "title" }}{{ .Tab }} settings{{ end }}
{{ define "content" }}
+
<div class="p-6">
+
<p class="text-xl font-bold dark:text-white">Settings</p>
+
</div>
+
<div class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
+
<section class="w-full grid grid-cols-1 md:grid-cols-4 gap-6">
+
<div class="col-span-1">
+
{{ template "user/settings/fragments/sidebar" . }}
+
</div>
+
<div class="col-span-1 md:col-span-3 flex flex-col gap-6">
+
{{ template "spindleList" . }}
+
</div>
+
</section>
+
</div>
+
{{ end }}
+
+
{{ define "spindleList" }}
+
<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>
+
{{ block "about" . }} {{ end }}
+
</div>
+
<div class="col-span-1 md:col-span-1 md:justify-self-end">
+
{{ template "docsButton" . }}
+
</div>
</div>
+
<section>
<div class="flex flex-col gap-6">
{{ block "list" . }} {{ end }}
{{ block "register" . }} {{ end }}
</div>
···
{{ define "about" }}
<section class="rounded flex items-center gap-2">
+
<p class="text-gray-500 dark:text-gray-400">
+
Spindles are small CI runners.
+
</p>
</section>
{{ end }}
{{ define "list" }}
+
<section class="rounded w-full flex flex-col gap-2">
+
<h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">your spindles</h2>
+
<div class="flex flex-col rounded border border-gray-200 dark:border-gray-700 w-full">
+
{{ range $spindle := .Spindles }}
+
{{ template "spindles/fragments/spindleListing" . }}
+
{{ else }}
+
<div class="flex items-center justify-center p-2 border-b border-gray-200 dark:border-gray-700 text-gray-500">
+
no spindles registered yet
</div>
+
{{ end }}
+
</div>
+
<div id="operation-error" class="text-red-500 dark:text-red-400"></div>
+
</section>
{{ end }}
{{ define "register" }}
+
<section class="rounded w-full lg:w-fit flex flex-col gap-2">
+
<h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">register a spindle</h2>
+
<p class="mb-2 dark:text-gray-300">Enter the hostname of your spindle to get started.</p>
+
<form
+
hx-post="/settings/spindles/register"
+
class="max-w-2xl mb-2 space-y-4"
+
hx-indicator="#register-button"
+
hx-swap="none"
+
>
+
<div class="flex gap-2">
+
<input
+
type="text"
+
id="instance"
+
name="instance"
+
placeholder="spindle.example.com"
+
required
+
class="flex-1 w-full dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400 px-3 py-2 border rounded"
+
>
+
<button
+
type="submit"
+
id="register-button"
+
class="btn rounded flex items-center py-2 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 group"
+
>
+
<span class="inline-flex items-center gap-2">
+
{{ i "plus" "w-4 h-4" }}
+
register
+
</span>
+
<span class="pl-2 hidden group-[.htmx-request]:inline">
+
{{ i "loader-circle" "w-4 h-4 animate-spin" }}
+
</span>
+
</button>
+
</div>
+
<div id="register-error" class="dark:text-red-400"></div>
+
</form>
+
+
</section>
+
{{ end }}
+
{{ define "docsButton" }}
+
<a
+
class="btn flex items-center gap-2"
+
href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/hosting.md">
+
{{ i "book" "size-4" }}
+
docs
+
</a>
+
<div
+
id="add-email-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">
+
</div>
{{ end }}
+2
appview/settings/settings.go
···
{"Name": "keys", "Icon": "key"},
{"Name": "emails", "Icon": "mail"},
{"Name": "notifications", "Icon": "bell"},
}
)
···
{"Name": "keys", "Icon": "key"},
{"Name": "emails", "Icon": "mail"},
{"Name": "notifications", "Icon": "bell"},
+
{"Name": "knots", "Icon": "volleyball"},
+
{"Name": "spindles", "Icon": "spool"},
}
)
+19 -2
appview/spindles/spindles.go
···
Logger *slog.Logger
}
func (s *Spindles) Router() http.Handler {
r := chi.NewRouter()
···
s.Pages.Spindles(w, pages.SpindlesParams{
LoggedInUser: user,
Spindles: all,
})
}
···
Spindle: spindle,
Members: members,
Repos: repoMap,
})
}
···
shouldRedirect := r.Header.Get("shouldRedirect")
if shouldRedirect == "true" {
-
s.Pages.HxRedirect(w, "/spindles")
return
}
···
}
// success
-
s.Pages.HxRedirect(w, fmt.Sprintf("/spindles/%s", instance))
}
func (s *Spindles) removeMember(w http.ResponseWriter, r *http.Request) {
···
Logger *slog.Logger
}
+
type tab = map[string]any
+
+
var (
+
spindlesTabs []tab = []tab{
+
{"Name": "profile", "Icon": "user"},
+
{"Name": "keys", "Icon": "key"},
+
{"Name": "emails", "Icon": "mail"},
+
{"Name": "notifications", "Icon": "bell"},
+
{"Name": "knots", "Icon": "volleyball"},
+
{"Name": "spindles", "Icon": "spool"},
+
}
+
)
+
func (s *Spindles) Router() http.Handler {
r := chi.NewRouter()
···
s.Pages.Spindles(w, pages.SpindlesParams{
LoggedInUser: user,
Spindles: all,
+
Tabs: spindlesTabs,
+
Tab: "spindles",
})
}
···
Spindle: spindle,
Members: members,
Repos: repoMap,
+
Tabs: spindlesTabs,
+
Tab: "spindles",
})
}
···
shouldRedirect := r.Header.Get("shouldRedirect")
if shouldRedirect == "true" {
+
s.Pages.HxRedirect(w, "/settings/spindles")
return
}
···
}
// success
+
s.Pages.HxRedirect(w, fmt.Sprintf("/settings/spindles/%s", instance))
}
func (s *Spindles) removeMember(w http.ResponseWriter, r *http.Request) {
+4 -2
appview/state/router.go
···
r.Mount("/settings", s.SettingsRouter())
r.Mount("/strings", s.StringsRouter(mw))
-
r.Mount("/knots", s.KnotsRouter())
-
r.Mount("/spindles", s.SpindlesRouter())
r.Mount("/notifications", s.NotificationsRouter(mw))
r.Mount("/signup", s.SignupRouter())
···
r.Mount("/settings", s.SettingsRouter())
r.Mount("/strings", s.StringsRouter(mw))
+
+
r.Mount("/settings/knots", s.KnotsRouter())
+
r.Mount("/settings/spindles", s.SpindlesRouter())
+
r.Mount("/notifications", s.NotificationsRouter(mw))
r.Mount("/signup", s.SignupRouter())
+2 -2
docs/hacking.md
···
with `ssh` exposed on port 2222.
Once the services are running, head to
-
http://localhost:3000/knots and hit verify. It should
verify the ownership of the services instantly if everything
went smoothly.
···
### running a spindle
The above VM should already be running a spindle on
-
`localhost:6555`. Head to http://localhost:3000/spindles and
hit verify. You can then configure each repository to use
this spindle and run CI jobs.
···
with `ssh` exposed on port 2222.
Once the services are running, head to
+
http://localhost:3000/settings/knots and hit verify. It should
verify the ownership of the services instantly if everything
went smoothly.
···
### running a spindle
The above VM should already be running a spindle on
+
`localhost:6555`. Head to http://localhost:3000/settings/spindles and
hit verify. You can then configure each repository to use
this spindle and run CI jobs.
+1 -1
docs/knot-hosting.md
···
You should now have a running knot server! You can finalize
your registration by hitting the `verify` button on the
-
[/knots](https://tangled.org/knots) page. This simply creates
a record on your PDS to announce the existence of the knot.
### custom paths
···
You should now have a running knot server! You can finalize
your registration by hitting the `verify` button on the
+
[/settings/knots](https://tangled.org/settings/knots) page. This simply creates
a record on your PDS to announce the existence of the knot.
### custom paths
+3 -3
docs/migrations.md
···
For knots:
- Upgrade to latest tag (v1.9.0 or above)
-
- Head to the [knot dashboard](https://tangled.org/knots) and
hit the "retry" button to verify your knot
For spindles:
- Upgrade to latest tag (v1.9.0 or above)
- Head to the [spindle
-
dashboard](https://tangled.org/spindles) and hit the
"retry" button to verify your spindle
## Upgrading from v1.7.x
···
[settings](https://tangled.org/settings) page.
- Restart your knot once you have replaced the environment
variable
-
- Head to the [knot dashboard](https://tangled.org/knots) and
hit the "retry" button to verify your knot. This simply
writes a `sh.tangled.knot` record to your PDS.
···
For knots:
- Upgrade to latest tag (v1.9.0 or above)
+
- Head to the [knot dashboard](https://tangled.org/settings/knots) and
hit the "retry" button to verify your knot
For spindles:
- Upgrade to latest tag (v1.9.0 or above)
- Head to the [spindle
+
dashboard](https://tangled.org/settings/spindles) and hit the
"retry" button to verify your spindle
## Upgrading from v1.7.x
···
[settings](https://tangled.org/settings) page.
- Restart your knot once you have replaced the environment
variable
+
- Head to the [knot dashboard](https://tangled.org/settings/knots) and
hit the "retry" button to verify your knot. This simply
writes a `sh.tangled.knot` record to your PDS.