+18
-1
appview/knots/knots.go
+18
-1
appview/knots/knots.go
···
39
Knotstream *eventconsumer.Consumer
40
}
41
42
func (k *Knots) Router() http.Handler {
43
r := chi.NewRouter()
44
···
70
k.Pages.Knots(w, pages.KnotsParams{
71
LoggedInUser: user,
72
Registrations: registrations,
73
})
74
}
75
···
132
Members: members,
133
Repos: repoMap,
134
IsOwner: true,
135
})
136
}
137
···
596
}
597
598
// success
599
-
k.Pages.HxRedirect(w, fmt.Sprintf("/knots/%s", domain))
600
}
601
602
func (k *Knots) removeMember(w http.ResponseWriter, r *http.Request) {
···
39
Knotstream *eventconsumer.Consumer
40
}
41
42
+
type tab = map[string]any
43
+
44
+
var (
45
+
knotsTabs []tab = []tab{
46
+
{"Name": "profile", "Icon": "user"},
47
+
{"Name": "keys", "Icon": "key"},
48
+
{"Name": "emails", "Icon": "mail"},
49
+
{"Name": "notifications", "Icon": "bell"},
50
+
{"Name": "knots", "Icon": "volleyball"},
51
+
{"Name": "spindles", "Icon": "spool"},
52
+
}
53
+
)
54
+
55
func (k *Knots) Router() http.Handler {
56
r := chi.NewRouter()
57
···
83
k.Pages.Knots(w, pages.KnotsParams{
84
LoggedInUser: user,
85
Registrations: registrations,
86
+
Tabs: knotsTabs,
87
+
Tab: "knots",
88
})
89
}
90
···
147
Members: members,
148
Repos: repoMap,
149
IsOwner: true,
150
+
Tabs: knotsTabs,
151
+
Tab: "knots",
152
})
153
}
154
···
613
}
614
615
// success
616
+
k.Pages.HxRedirect(w, fmt.Sprintf("/settings/knots/%s", domain))
617
}
618
619
func (k *Knots) removeMember(w http.ResponseWriter, r *http.Request) {
+10
appview/pages/pages.go
+10
appview/pages/pages.go
···
407
type KnotsParams struct {
408
LoggedInUser *oauth.User
409
Registrations []models.Registration
410
}
411
412
func (p *Pages) Knots(w io.Writer, params KnotsParams) error {
···
419
Members []string
420
Repos map[string][]models.Repo
421
IsOwner bool
422
}
423
424
func (p *Pages) Knot(w io.Writer, params KnotParams) error {
···
436
type SpindlesParams struct {
437
LoggedInUser *oauth.User
438
Spindles []models.Spindle
439
}
440
441
func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error {
···
444
445
type SpindleListingParams struct {
446
models.Spindle
447
}
448
449
func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error {
···
455
Spindle models.Spindle
456
Members []string
457
Repos map[string][]models.Repo
458
}
459
460
func (p *Pages) SpindleDashboard(w io.Writer, params SpindleDashboardParams) error {
···
407
type KnotsParams struct {
408
LoggedInUser *oauth.User
409
Registrations []models.Registration
410
+
Tabs []map[string]any
411
+
Tab string
412
}
413
414
func (p *Pages) Knots(w io.Writer, params KnotsParams) error {
···
421
Members []string
422
Repos map[string][]models.Repo
423
IsOwner bool
424
+
Tabs []map[string]any
425
+
Tab string
426
}
427
428
func (p *Pages) Knot(w io.Writer, params KnotParams) error {
···
440
type SpindlesParams struct {
441
LoggedInUser *oauth.User
442
Spindles []models.Spindle
443
+
Tabs []map[string]any
444
+
Tab string
445
}
446
447
func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error {
···
450
451
type SpindleListingParams struct {
452
models.Spindle
453
+
Tabs []map[string]any
454
+
Tab string
455
}
456
457
func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error {
···
463
Spindle models.Spindle
464
Members []string
465
Repos map[string][]models.Repo
466
+
Tabs []map[string]any
467
+
Tab string
468
}
469
470
func (p *Pages) SpindleDashboard(w io.Writer, params SpindleDashboardParams) error {
+23
-7
appview/pages/templates/knots/dashboard.html
+23
-7
appview/pages/templates/knots/dashboard.html
···
1
-
{{ define "title" }}{{ .Registration.Domain }} · knots{{ end }}
2
3
{{ define "content" }}
4
-
<div class="px-6 py-4">
5
<div class="flex justify-between items-center">
6
-
<h1 class="text-xl font-bold dark:text-white">{{ .Registration.Domain }}</h1>
7
<div id="right-side" class="flex gap-2">
8
{{ $style := "px-2 py-1 rounded flex items-center flex-shrink-0 gap-2" }}
9
{{ $isOwner := and .LoggedInUser (eq .LoggedInUser.Did .Registration.ByDid) }}
···
35
</div>
36
37
{{ if .Members }}
38
-
<section class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
39
<div class="flex flex-col gap-2">
40
{{ block "member" . }} {{ end }}
41
</div>
···
79
<button
80
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
81
title="Delete knot"
82
-
hx-delete="/knots/{{ .Domain }}"
83
hx-swap="outerHTML"
84
hx-confirm="Are you sure you want to delete the knot '{{ .Domain }}'?"
85
hx-headers='{"shouldRedirect": "true"}'
···
95
<button
96
class="btn gap-2 group"
97
title="Retry knot verification"
98
-
hx-post="/knots/{{ .Domain }}/retry"
99
hx-swap="none"
100
hx-headers='{"shouldRefresh": "true"}'
101
>
···
113
<button
114
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
115
title="Remove member"
116
-
hx-post="/knots/{{ $root.Registration.Domain }}/remove"
117
hx-swap="none"
118
hx-vals='{"member": "{{$member}}" }'
119
hx-confirm="Are you sure you want to remove {{ $memberHandle }} from this knot?"
···
1
+
{{ define "title" }}{{ .Registration.Domain }} · {{ .Tab }} settings{{ end }}
2
3
{{ define "content" }}
4
+
<div class="p-6">
5
+
<p class="text-xl font-bold dark:text-white">Settings</p>
6
+
</div>
7
+
<div class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
8
+
<section class="w-full grid grid-cols-1 md:grid-cols-4 gap-6">
9
+
<div class="col-span-1">
10
+
{{ template "user/settings/fragments/sidebar" . }}
11
+
</div>
12
+
<div class="col-span-1 md:col-span-3 flex flex-col gap-6">
13
+
{{ template "knotDash" . }}
14
+
</div>
15
+
</section>
16
+
</div>
17
+
{{ end }}
18
+
19
+
{{ define "knotDash" }}
20
+
<div>
21
<div class="flex justify-between items-center">
22
+
<h2 class="text-sm pb-2 uppercase font-bold">{{ .Tab }} · {{ .Registration.Domain }}</h2>
23
<div id="right-side" class="flex gap-2">
24
{{ $style := "px-2 py-1 rounded flex items-center flex-shrink-0 gap-2" }}
25
{{ $isOwner := and .LoggedInUser (eq .LoggedInUser.Did .Registration.ByDid) }}
···
51
</div>
52
53
{{ if .Members }}
54
+
<section class="bg-white dark:bg-gray-800 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
55
<div class="flex flex-col gap-2">
56
{{ block "member" . }} {{ end }}
57
</div>
···
95
<button
96
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
97
title="Delete knot"
98
+
hx-delete="/settings/knots/{{ .Domain }}"
99
hx-swap="outerHTML"
100
hx-confirm="Are you sure you want to delete the knot '{{ .Domain }}'?"
101
hx-headers='{"shouldRedirect": "true"}'
···
111
<button
112
class="btn gap-2 group"
113
title="Retry knot verification"
114
+
hx-post="/settings/knots/{{ .Domain }}/retry"
115
hx-swap="none"
116
hx-headers='{"shouldRefresh": "true"}'
117
>
···
129
<button
130
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
131
title="Remove member"
132
+
hx-post="/settings/knots/{{ $root.Registration.Domain }}/remove"
133
hx-swap="none"
134
hx-vals='{"member": "{{$member}}" }'
135
hx-confirm="Are you sure you want to remove {{ $memberHandle }} from this knot?"
+1
-1
appview/pages/templates/knots/fragments/addMemberModal.html
+1
-1
appview/pages/templates/knots/fragments/addMemberModal.html
+3
-3
appview/pages/templates/knots/fragments/knotListing.html
+3
-3
appview/pages/templates/knots/fragments/knotListing.html
···
7
8
{{ define "knotLeftSide" }}
9
{{ if .Registered }}
10
-
<a href="/knots/{{ .Domain }}" class="hover:no-underline flex items-center gap-2 min-w-0 max-w-[60%]">
11
{{ i "hard-drive" "w-4 h-4" }}
12
<span class="hover:underline">
13
{{ .Domain }}
···
56
<button
57
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
58
title="Delete knot"
59
-
hx-delete="/knots/{{ .Domain }}"
60
hx-swap="outerHTML"
61
hx-target="#knot-{{.Id}}"
62
hx-confirm="Are you sure you want to delete the knot '{{ .Domain }}'?"
···
72
<button
73
class="btn gap-2 group"
74
title="Retry knot verification"
75
-
hx-post="/knots/{{ .Domain }}/retry"
76
hx-swap="none"
77
hx-target="#knot-{{.Id}}"
78
>
···
7
8
{{ define "knotLeftSide" }}
9
{{ if .Registered }}
10
+
<a href="/settings/knots/{{ .Domain }}" class="hover:no-underline flex items-center gap-2 min-w-0 max-w-[60%]">
11
{{ i "hard-drive" "w-4 h-4" }}
12
<span class="hover:underline">
13
{{ .Domain }}
···
56
<button
57
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
58
title="Delete knot"
59
+
hx-delete="/settings/knots/{{ .Domain }}"
60
hx-swap="outerHTML"
61
hx-target="#knot-{{.Id}}"
62
hx-confirm="Are you sure you want to delete the knot '{{ .Domain }}'?"
···
72
<button
73
class="btn gap-2 group"
74
title="Retry knot verification"
75
+
hx-post="/settings/knots/{{ .Domain }}/retry"
76
hx-swap="none"
77
hx-target="#knot-{{.Id}}"
78
>
+42
-11
appview/pages/templates/knots/index.html
+42
-11
appview/pages/templates/knots/index.html
···
1
-
{{ define "title" }}knots{{ end }}
2
3
{{ define "content" }}
4
-
<div class="px-6 py-4 flex items-center justify-between gap-4 align-bottom">
5
-
<h1 class="text-xl font-bold dark:text-white">Knots</h1>
6
-
<span class="flex items-center gap-1">
7
-
{{ i "book" "w-3 h-3" }}
8
-
<a href="https://tangled.org/@tangled.org/core/blob/master/docs/knot-hosting.md">docs</a>
9
-
</span>
10
-
</div>
11
12
-
<section class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
13
<div class="flex flex-col gap-6">
14
-
{{ block "about" . }} {{ end }}
15
{{ block "list" . }} {{ end }}
16
{{ block "register" . }} {{ end }}
17
</div>
···
50
<h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">register a knot</h2>
51
<p class="mb-2 dark:text-gray-300">Enter the hostname of your knot to get started.</p>
52
<form
53
-
hx-post="/knots/register"
54
class="max-w-2xl mb-2 space-y-4"
55
hx-indicator="#register-button"
56
hx-swap="none"
···
84
85
</section>
86
{{ end }}
···
1
+
{{ define "title" }}{{ .Tab }} settings{{ end }}
2
3
{{ define "content" }}
4
+
<div class="p-6">
5
+
<p class="text-xl font-bold dark:text-white">Settings</p>
6
+
</div>
7
+
<div class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
8
+
<section class="w-full grid grid-cols-1 md:grid-cols-4 gap-6">
9
+
<div class="col-span-1">
10
+
{{ template "user/settings/fragments/sidebar" . }}
11
+
</div>
12
+
<div class="col-span-1 md:col-span-3 flex flex-col gap-6">
13
+
{{ template "knotsList" . }}
14
+
</div>
15
+
</section>
16
+
</div>
17
+
{{ end }}
18
+
19
+
{{ define "knotsList" }}
20
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center">
21
+
<div class="col-span-1 md:col-span-2">
22
+
<h2 class="text-sm pb-2 uppercase font-bold">Knots</h2>
23
+
{{ block "about" . }} {{ end }}
24
+
</div>
25
+
<div class="col-span-1 md:col-span-1 md:justify-self-end">
26
+
{{ template "docsButton" . }}
27
+
</div>
28
+
</div>
29
30
+
<section>
31
<div class="flex flex-col gap-6">
32
{{ block "list" . }} {{ end }}
33
{{ block "register" . }} {{ end }}
34
</div>
···
67
<h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">register a knot</h2>
68
<p class="mb-2 dark:text-gray-300">Enter the hostname of your knot to get started.</p>
69
<form
70
+
hx-post="/settings/knots/register"
71
class="max-w-2xl mb-2 space-y-4"
72
hx-indicator="#register-button"
73
hx-swap="none"
···
101
102
</section>
103
{{ end }}
104
+
105
+
{{ define "docsButton" }}
106
+
<a
107
+
class="btn flex items-center gap-2"
108
+
href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/hosting.md">
109
+
{{ i "book" "size-4" }}
110
+
docs
111
+
</a>
112
+
<div
113
+
id="add-email-modal"
114
+
popover
115
+
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">
116
+
</div>
117
+
{{ end }}
-2
appview/pages/templates/layouts/fragments/topbar.html
-2
appview/pages/templates/layouts/fragments/topbar.html
+1
-1
appview/pages/templates/repo/fork.html
+1
-1
appview/pages/templates/repo/fork.html
+1
-1
appview/pages/templates/repo/new.html
+1
-1
appview/pages/templates/repo/new.html
+22
-6
appview/pages/templates/spindles/dashboard.html
+22
-6
appview/pages/templates/spindles/dashboard.html
···
1
-
{{ define "title" }}{{.Spindle.Instance}} · spindles{{ end }}
2
3
{{ define "content" }}
4
-
<div class="px-6 py-4">
5
<div class="flex justify-between items-center">
6
-
<h1 class="text-xl font-bold dark:text-white">{{ .Spindle.Instance }}</h1>
7
<div id="right-side" class="flex gap-2">
8
{{ $style := "px-2 py-1 rounded flex items-center flex-shrink-0 gap-2" }}
9
{{ $isOwner := and .LoggedInUser (eq .LoggedInUser.Did .Spindle.Owner) }}
···
71
<button
72
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
73
title="Delete spindle"
74
-
hx-delete="/spindles/{{ .Instance }}"
75
hx-swap="outerHTML"
76
hx-confirm="Are you sure you want to delete the spindle '{{ .Instance }}'?"
77
hx-headers='{"shouldRedirect": "true"}'
···
87
<button
88
class="btn gap-2 group"
89
title="Retry spindle verification"
90
-
hx-post="/spindles/{{ .Instance }}/retry"
91
hx-swap="none"
92
hx-headers='{"shouldRefresh": "true"}'
93
>
···
104
<button
105
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
106
title="Remove member"
107
-
hx-post="/spindles/{{ $root.Spindle.Instance }}/remove"
108
hx-swap="none"
109
hx-vals='{"member": "{{$member}}" }'
110
hx-confirm="Are you sure you want to remove {{ resolve $member }} from this instance?"
···
1
+
{{ define "title" }}{{.Spindle.Instance}} · {{ .Tab }} settings{{ end }}
2
3
{{ define "content" }}
4
+
<div class="p-6">
5
+
<p class="text-xl font-bold dark:text-white">Settings</p>
6
+
</div>
7
+
<div class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
8
+
<section class="w-full grid grid-cols-1 md:grid-cols-4 gap-6">
9
+
<div class="col-span-1">
10
+
{{ template "user/settings/fragments/sidebar" . }}
11
+
</div>
12
+
<div class="col-span-1 md:col-span-3 flex flex-col gap-6">
13
+
{{ template "spindleDash" . }}
14
+
</div>
15
+
</section>
16
+
</div>
17
+
{{ end }}
18
+
19
+
{{ define "spindleDash" }}
20
+
<div>
21
<div class="flex justify-between items-center">
22
+
<h2 class="text-sm pb-2 uppercase font-bold">{{ .Tab }} · {{ .Spindle.Instance }}</h2>
23
<div id="right-side" class="flex gap-2">
24
{{ $style := "px-2 py-1 rounded flex items-center flex-shrink-0 gap-2" }}
25
{{ $isOwner := and .LoggedInUser (eq .LoggedInUser.Did .Spindle.Owner) }}
···
87
<button
88
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
89
title="Delete spindle"
90
+
hx-delete="/settings/spindles/{{ .Instance }}"
91
hx-swap="outerHTML"
92
hx-confirm="Are you sure you want to delete the spindle '{{ .Instance }}'?"
93
hx-headers='{"shouldRedirect": "true"}'
···
103
<button
104
class="btn gap-2 group"
105
title="Retry spindle verification"
106
+
hx-post="/settings/spindles/{{ .Instance }}/retry"
107
hx-swap="none"
108
hx-headers='{"shouldRefresh": "true"}'
109
>
···
120
<button
121
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
122
title="Remove member"
123
+
hx-post="/settings/spindles/{{ $root.Spindle.Instance }}/remove"
124
hx-swap="none"
125
hx-vals='{"member": "{{$member}}" }'
126
hx-confirm="Are you sure you want to remove {{ resolve $member }} from this instance?"
+1
-1
appview/pages/templates/spindles/fragments/addMemberModal.html
+1
-1
appview/pages/templates/spindles/fragments/addMemberModal.html
+3
-3
appview/pages/templates/spindles/fragments/spindleListing.html
+3
-3
appview/pages/templates/spindles/fragments/spindleListing.html
···
7
8
{{ define "spindleLeftSide" }}
9
{{ if .Verified }}
10
-
<a href="/spindles/{{ .Instance }}" class="hover:no-underline flex items-center gap-2 min-w-0 max-w-[60%]">
11
{{ i "hard-drive" "w-4 h-4" }}
12
<span class="hover:underline">
13
{{ .Instance }}
···
50
<button
51
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
52
title="Delete spindle"
53
-
hx-delete="/spindles/{{ .Instance }}"
54
hx-swap="outerHTML"
55
hx-target="#spindle-{{.Id}}"
56
hx-confirm="Are you sure you want to delete the spindle '{{ .Instance }}'?"
···
66
<button
67
class="btn gap-2 group"
68
title="Retry spindle verification"
69
-
hx-post="/spindles/{{ .Instance }}/retry"
70
hx-swap="none"
71
hx-target="#spindle-{{.Id}}"
72
>
···
7
8
{{ define "spindleLeftSide" }}
9
{{ if .Verified }}
10
+
<a href="/settings/spindles/{{ .Instance }}" class="hover:no-underline flex items-center gap-2 min-w-0 max-w-[60%]">
11
{{ i "hard-drive" "w-4 h-4" }}
12
<span class="hover:underline">
13
{{ .Instance }}
···
50
<button
51
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
52
title="Delete spindle"
53
+
hx-delete="/settings/spindles/{{ .Instance }}"
54
hx-swap="outerHTML"
55
hx-target="#spindle-{{.Id}}"
56
hx-confirm="Are you sure you want to delete the spindle '{{ .Instance }}'?"
···
66
<button
67
class="btn gap-2 group"
68
title="Retry spindle verification"
69
+
hx-post="/settings/spindles/{{ .Instance }}/retry"
70
hx-swap="none"
71
hx-target="#spindle-{{.Id}}"
72
>
+90
-59
appview/pages/templates/spindles/index.html
+90
-59
appview/pages/templates/spindles/index.html
···
1
-
{{ define "title" }}spindles{{ end }}
2
3
{{ define "content" }}
4
-
<div class="px-6 py-4 flex items-center justify-between gap-4 align-bottom">
5
-
<h1 class="text-xl font-bold dark:text-white">Spindles</h1>
6
-
<span class="flex items-center gap-1">
7
-
{{ i "book" "w-3 h-3" }}
8
-
<a href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/hosting.md">docs</a>
9
-
</span>
10
</div>
11
12
-
<section class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
13
<div class="flex flex-col gap-6">
14
-
{{ block "about" . }} {{ end }}
15
{{ block "list" . }} {{ end }}
16
{{ block "register" . }} {{ end }}
17
</div>
···
20
21
{{ define "about" }}
22
<section class="rounded flex items-center gap-2">
23
-
<p class="text-gray-500 dark:text-gray-400">
24
-
Spindles are small CI runners.
25
-
</p>
26
</section>
27
{{ end }}
28
29
{{ define "list" }}
30
-
<section class="rounded w-full flex flex-col gap-2">
31
-
<h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">your spindles</h2>
32
-
<div class="flex flex-col rounded border border-gray-200 dark:border-gray-700 w-full">
33
-
{{ range $spindle := .Spindles }}
34
-
{{ template "spindles/fragments/spindleListing" . }}
35
-
{{ else }}
36
-
<div class="flex items-center justify-center p-2 border-b border-gray-200 dark:border-gray-700 text-gray-500">
37
-
no spindles registered yet
38
-
</div>
39
-
{{ end }}
40
</div>
41
-
<div id="operation-error" class="text-red-500 dark:text-red-400"></div>
42
-
</section>
43
{{ end }}
44
45
{{ define "register" }}
46
-
<section class="rounded w-full lg:w-fit flex flex-col gap-2">
47
-
<h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">register a spindle</h2>
48
-
<p class="mb-2 dark:text-gray-300">Enter the hostname of your spindle to get started.</p>
49
-
<form
50
-
hx-post="/spindles/register"
51
-
class="max-w-2xl mb-2 space-y-4"
52
-
hx-indicator="#register-button"
53
-
hx-swap="none"
54
-
>
55
-
<div class="flex gap-2">
56
-
<input
57
-
type="text"
58
-
id="instance"
59
-
name="instance"
60
-
placeholder="spindle.example.com"
61
-
required
62
-
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"
63
-
>
64
-
<button
65
-
type="submit"
66
-
id="register-button"
67
-
class="btn rounded flex items-center py-2 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 group"
68
-
>
69
-
<span class="inline-flex items-center gap-2">
70
-
{{ i "plus" "w-4 h-4" }}
71
-
register
72
-
</span>
73
-
<span class="pl-2 hidden group-[.htmx-request]:inline">
74
-
{{ i "loader-circle" "w-4 h-4 animate-spin" }}
75
-
</span>
76
-
</button>
77
-
</div>
78
79
-
<div id="register-error" class="dark:text-red-400"></div>
80
-
</form>
81
82
-
</section>
83
{{ end }}
···
1
+
{{ define "title" }}{{ .Tab }} settings{{ end }}
2
3
{{ define "content" }}
4
+
<div class="p-6">
5
+
<p class="text-xl font-bold dark:text-white">Settings</p>
6
+
</div>
7
+
<div class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
8
+
<section class="w-full grid grid-cols-1 md:grid-cols-4 gap-6">
9
+
<div class="col-span-1">
10
+
{{ template "user/settings/fragments/sidebar" . }}
11
+
</div>
12
+
<div class="col-span-1 md:col-span-3 flex flex-col gap-6">
13
+
{{ template "spindleList" . }}
14
+
</div>
15
+
</section>
16
+
</div>
17
+
{{ end }}
18
+
19
+
{{ define "spindleList" }}
20
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center">
21
+
<div class="col-span-1 md:col-span-2">
22
+
<h2 class="text-sm pb-2 uppercase font-bold">Spindle</h2>
23
+
{{ block "about" . }} {{ end }}
24
+
</div>
25
+
<div class="col-span-1 md:col-span-1 md:justify-self-end">
26
+
{{ template "docsButton" . }}
27
+
</div>
28
</div>
29
30
+
<section>
31
<div class="flex flex-col gap-6">
32
{{ block "list" . }} {{ end }}
33
{{ block "register" . }} {{ end }}
34
</div>
···
37
38
{{ define "about" }}
39
<section class="rounded flex items-center gap-2">
40
+
<p class="text-gray-500 dark:text-gray-400">
41
+
Spindles are small CI runners.
42
+
</p>
43
</section>
44
{{ end }}
45
46
{{ define "list" }}
47
+
<section class="rounded w-full flex flex-col gap-2">
48
+
<h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">your spindles</h2>
49
+
<div class="flex flex-col rounded border border-gray-200 dark:border-gray-700 w-full">
50
+
{{ range $spindle := .Spindles }}
51
+
{{ template "spindles/fragments/spindleListing" . }}
52
+
{{ else }}
53
+
<div class="flex items-center justify-center p-2 border-b border-gray-200 dark:border-gray-700 text-gray-500">
54
+
no spindles registered yet
55
</div>
56
+
{{ end }}
57
+
</div>
58
+
<div id="operation-error" class="text-red-500 dark:text-red-400"></div>
59
+
</section>
60
{{ end }}
61
62
{{ define "register" }}
63
+
<section class="rounded w-full lg:w-fit flex flex-col gap-2">
64
+
<h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">register a spindle</h2>
65
+
<p class="mb-2 dark:text-gray-300">Enter the hostname of your spindle to get started.</p>
66
+
<form
67
+
hx-post="/settings/spindles/register"
68
+
class="max-w-2xl mb-2 space-y-4"
69
+
hx-indicator="#register-button"
70
+
hx-swap="none"
71
+
>
72
+
<div class="flex gap-2">
73
+
<input
74
+
type="text"
75
+
id="instance"
76
+
name="instance"
77
+
placeholder="spindle.example.com"
78
+
required
79
+
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"
80
+
>
81
+
<button
82
+
type="submit"
83
+
id="register-button"
84
+
class="btn rounded flex items-center py-2 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 group"
85
+
>
86
+
<span class="inline-flex items-center gap-2">
87
+
{{ i "plus" "w-4 h-4" }}
88
+
register
89
+
</span>
90
+
<span class="pl-2 hidden group-[.htmx-request]:inline">
91
+
{{ i "loader-circle" "w-4 h-4 animate-spin" }}
92
+
</span>
93
+
</button>
94
+
</div>
95
96
+
<div id="register-error" class="dark:text-red-400"></div>
97
+
</form>
98
+
99
+
</section>
100
+
{{ end }}
101
102
+
{{ define "docsButton" }}
103
+
<a
104
+
class="btn flex items-center gap-2"
105
+
href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/hosting.md">
106
+
{{ i "book" "size-4" }}
107
+
docs
108
+
</a>
109
+
<div
110
+
id="add-email-modal"
111
+
popover
112
+
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">
113
+
</div>
114
{{ end }}
+2
appview/settings/settings.go
+2
appview/settings/settings.go
+19
-2
appview/spindles/spindles.go
+19
-2
appview/spindles/spindles.go
···
38
Logger *slog.Logger
39
}
40
41
func (s *Spindles) Router() http.Handler {
42
r := chi.NewRouter()
43
···
69
s.Pages.Spindles(w, pages.SpindlesParams{
70
LoggedInUser: user,
71
Spindles: all,
72
})
73
}
74
···
127
Spindle: spindle,
128
Members: members,
129
Repos: repoMap,
130
})
131
}
132
···
365
366
shouldRedirect := r.Header.Get("shouldRedirect")
367
if shouldRedirect == "true" {
368
-
s.Pages.HxRedirect(w, "/spindles")
369
return
370
}
371
···
581
}
582
583
// success
584
-
s.Pages.HxRedirect(w, fmt.Sprintf("/spindles/%s", instance))
585
}
586
587
func (s *Spindles) removeMember(w http.ResponseWriter, r *http.Request) {
···
38
Logger *slog.Logger
39
}
40
41
+
type tab = map[string]any
42
+
43
+
var (
44
+
spindlesTabs []tab = []tab{
45
+
{"Name": "profile", "Icon": "user"},
46
+
{"Name": "keys", "Icon": "key"},
47
+
{"Name": "emails", "Icon": "mail"},
48
+
{"Name": "notifications", "Icon": "bell"},
49
+
{"Name": "knots", "Icon": "volleyball"},
50
+
{"Name": "spindles", "Icon": "spool"},
51
+
}
52
+
)
53
+
54
func (s *Spindles) Router() http.Handler {
55
r := chi.NewRouter()
56
···
82
s.Pages.Spindles(w, pages.SpindlesParams{
83
LoggedInUser: user,
84
Spindles: all,
85
+
Tabs: spindlesTabs,
86
+
Tab: "spindles",
87
})
88
}
89
···
142
Spindle: spindle,
143
Members: members,
144
Repos: repoMap,
145
+
Tabs: spindlesTabs,
146
+
Tab: "spindles",
147
})
148
}
149
···
382
383
shouldRedirect := r.Header.Get("shouldRedirect")
384
if shouldRedirect == "true" {
385
+
s.Pages.HxRedirect(w, "/settings/spindles")
386
return
387
}
388
···
598
}
599
600
// success
601
+
s.Pages.HxRedirect(w, fmt.Sprintf("/settings/spindles/%s", instance))
602
}
603
604
func (s *Spindles) removeMember(w http.ResponseWriter, r *http.Request) {
+4
-2
appview/state/router.go
+4
-2
appview/state/router.go
···
166
167
r.Mount("/settings", s.SettingsRouter())
168
r.Mount("/strings", s.StringsRouter(mw))
169
+
170
+
r.Mount("/settings/knots", s.KnotsRouter())
171
+
r.Mount("/settings/spindles", s.SpindlesRouter())
172
+
173
r.Mount("/notifications", s.NotificationsRouter(mw))
174
175
r.Mount("/signup", s.SignupRouter())
+2
-2
docs/hacking.md
+2
-2
docs/hacking.md
···
121
with `ssh` exposed on port 2222.
122
123
Once the services are running, head to
124
-
http://localhost:3000/knots and hit verify. It should
125
verify the ownership of the services instantly if everything
126
went smoothly.
127
···
146
### running a spindle
147
148
The above VM should already be running a spindle on
149
-
`localhost:6555`. Head to http://localhost:3000/spindles and
150
hit verify. You can then configure each repository to use
151
this spindle and run CI jobs.
152
···
121
with `ssh` exposed on port 2222.
122
123
Once the services are running, head to
124
+
http://localhost:3000/settings/knots and hit verify. It should
125
verify the ownership of the services instantly if everything
126
went smoothly.
127
···
146
### running a spindle
147
148
The above VM should already be running a spindle on
149
+
`localhost:6555`. Head to http://localhost:3000/settings/spindles and
150
hit verify. You can then configure each repository to use
151
this spindle and run CI jobs.
152
+1
-1
docs/knot-hosting.md
+1
-1
docs/knot-hosting.md
···
131
132
You should now have a running knot server! You can finalize
133
your registration by hitting the `verify` button on the
134
+
[/settings/knots](https://tangled.org/settings/knots) page. This simply creates
135
a record on your PDS to announce the existence of the knot.
136
137
### custom paths
+3
-3
docs/migrations.md
+3
-3
docs/migrations.md
···
14
For knots:
15
16
- Upgrade to latest tag (v1.9.0 or above)
17
-
- Head to the [knot dashboard](https://tangled.org/knots) and
18
hit the "retry" button to verify your knot
19
20
For spindles:
21
22
- Upgrade to latest tag (v1.9.0 or above)
23
- Head to the [spindle
24
-
dashboard](https://tangled.org/spindles) and hit the
25
"retry" button to verify your spindle
26
27
## Upgrading from v1.7.x
···
41
[settings](https://tangled.org/settings) page.
42
- Restart your knot once you have replaced the environment
43
variable
44
-
- Head to the [knot dashboard](https://tangled.org/knots) and
45
hit the "retry" button to verify your knot. This simply
46
writes a `sh.tangled.knot` record to your PDS.
47
···
14
For knots:
15
16
- Upgrade to latest tag (v1.9.0 or above)
17
+
- Head to the [knot dashboard](https://tangled.org/settings/knots) and
18
hit the "retry" button to verify your knot
19
20
For spindles:
21
22
- Upgrade to latest tag (v1.9.0 or above)
23
- Head to the [spindle
24
+
dashboard](https://tangled.org/settings/spindles) and hit the
25
"retry" button to verify your spindle
26
27
## Upgrading from v1.7.x
···
41
[settings](https://tangled.org/settings) page.
42
- Restart your knot once you have replaced the environment
43
variable
44
+
- Head to the [knot dashboard](https://tangled.org/settings/knots) and
45
hit the "retry" button to verify your knot. This simply
46
writes a `sh.tangled.knot` record to your PDS.
47