mirror of
https://github.com/amir20/dozzle.git
synced 2026-01-04 20:14:59 +01:00
feat: supports having custom content with markdown (#2466)
This commit is contained in:
1
assets/components.d.ts
vendored
1
assets/components.d.ts
vendored
@@ -35,6 +35,7 @@ declare module 'vue' {
|
||||
'Ic:sharpKeyboardReturn': typeof import('~icons/ic/sharp-keyboard-return')['default']
|
||||
InfiniteLoader: typeof import('./components/InfiniteLoader.vue')['default']
|
||||
KeyShortcut: typeof import('./components/common/KeyShortcut.vue')['default']
|
||||
Links: typeof import('./components/common/Links.vue')['default']
|
||||
LogActionsToolbar: typeof import('./components/LogViewer/LogActionsToolbar.vue')['default']
|
||||
LogContainer: typeof import('./components/LogViewer/LogContainer.vue')['default']
|
||||
LogDate: typeof import('./components/LogViewer/LogDate.vue')['default']
|
||||
|
||||
33
assets/components/common/Links.vue
Normal file
33
assets/components/common/Links.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<template v-if="config.pages">
|
||||
<router-link
|
||||
:to="{ name: 'content-id', params: { id: page.id } }"
|
||||
:title="page.title"
|
||||
v-for="page in config.pages"
|
||||
:key="page.id"
|
||||
class="link-primary"
|
||||
>
|
||||
{{ page.title }}
|
||||
</router-link>
|
||||
</template>
|
||||
<template v-if="config.user">
|
||||
<div v-if="config.authProvider === 'simple'">
|
||||
<button @click.prevent="logout()" class="link-primary">{{ $t("button.logout") }}</button>
|
||||
</div>
|
||||
<div>
|
||||
{{ config.user.name ? config.user.name : config.user.email }}
|
||||
</div>
|
||||
<img class="h-10 w-10 rounded-full p-1 ring-2 ring-base-content/50" :src="config.user.avatar" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
async function logout() {
|
||||
await fetch(withBase("/api/token"), {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
location.reload();
|
||||
}
|
||||
</script>
|
||||
@@ -8,7 +8,9 @@
|
||||
<pane min-size="10">
|
||||
<splitpanes>
|
||||
<pane class="router-view min-h-screen">
|
||||
<router-view></router-view>
|
||||
<Suspense>
|
||||
<router-view></router-view>
|
||||
</Suspense>
|
||||
</pane>
|
||||
<template v-if="!isMobile">
|
||||
<pane v-for="other in activeContainers" :key="other.id">
|
||||
|
||||
@@ -118,4 +118,8 @@
|
||||
--nc: var(--pc);
|
||||
@apply active;
|
||||
}
|
||||
|
||||
.link-primary {
|
||||
@apply underline-offset-4 hover:underline;
|
||||
}
|
||||
}
|
||||
|
||||
21
assets/pages/content/[id].vue
Normal file
21
assets/pages/content/[id].vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-8 p-8">
|
||||
<section>
|
||||
<links />
|
||||
</section>
|
||||
<section>
|
||||
<article class="prose" v-html="data!.content"></article>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const { id } = defineProps<{ id: string }>();
|
||||
|
||||
const { data } = await useFetch(() => withBase("/api/content/" + id), {
|
||||
refetch: true,
|
||||
})
|
||||
.get()
|
||||
.json<{ title: string; content: string }>();
|
||||
</script>
|
||||
<style lang="postcss" scoped></style>
|
||||
@@ -1,15 +1,7 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-16 px-4 pt-8 md:px-8">
|
||||
<section v-if="config.user">
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<div v-if="config.authProvider === 'simple'">
|
||||
<button @click.prevent="logout()" class="link-primary">{{ $t("button.logout") }}</button>
|
||||
</div>
|
||||
<div>
|
||||
{{ config.user.name ? config.user.name : config.user.email }}
|
||||
</div>
|
||||
<img class="h-10 w-10 rounded-full p-1 ring-2 ring-base-content/50" :src="config.user.avatar" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-16 p-8">
|
||||
<section>
|
||||
<links />
|
||||
</section>
|
||||
<section>
|
||||
<div class="stats grid bg-base-lighter shadow">
|
||||
@@ -76,14 +68,6 @@ watchEffect(() => {
|
||||
setTitle(t("title.dashboard", { count: runningContainers.length }));
|
||||
}
|
||||
});
|
||||
|
||||
async function logout() {
|
||||
await fetch(withBase("/api/token"), {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
location.reload();
|
||||
}
|
||||
</script>
|
||||
<style lang="postcss" scoped>
|
||||
:deep(tr td) {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<div class="mt-10 flex flex-col gap-8 px-4 md:px-8">
|
||||
<div class="flex flex-col gap-8 p-8">
|
||||
<section>
|
||||
<links />
|
||||
</section>
|
||||
<section>
|
||||
<div class="has-underline">
|
||||
<h2>{{ $t("settings.about") }}</h2>
|
||||
|
||||
@@ -18,6 +18,7 @@ interface Config {
|
||||
avatar: string;
|
||||
};
|
||||
serverSettings?: Settings;
|
||||
pages?: { id: string; title: string }[];
|
||||
}
|
||||
|
||||
const pageConfig = JSON.parse(text);
|
||||
|
||||
3
go.mod
3
go.mod
@@ -29,6 +29,8 @@ require (
|
||||
require (
|
||||
github.com/go-chi/chi/v5 v5.0.10
|
||||
github.com/go-chi/jwtauth/v5 v5.1.1
|
||||
github.com/yuin/goldmark v1.6.0
|
||||
github.com/yuin/goldmark-meta v1.1.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -53,6 +55,7 @@ require (
|
||||
golang.org/x/text v0.12.0 // indirect
|
||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect
|
||||
golang.org/x/tools v0.12.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.3.0 // indirect
|
||||
gotest.tools/v3 v3.0.3 // indirect
|
||||
)
|
||||
|
||||
|
||||
6
go.sum
6
go.sum
@@ -228,6 +228,10 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
|
||||
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
|
||||
github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
@@ -560,6 +564,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
67
internal/content/disk.go
Normal file
67
internal/content/disk.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package content
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
meta "github.com/yuin/goldmark-meta"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
)
|
||||
|
||||
type Page struct {
|
||||
Id string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content,omitempty"`
|
||||
}
|
||||
|
||||
func ReadAll() ([]*Page, error) {
|
||||
var pages []*Page
|
||||
files, err := filepath.Glob("data/content/*.md")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading /data/content/*.md: %w", err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
id := filepath.Base(file)
|
||||
id = id[0 : len(id)-3]
|
||||
page, err := Read(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading %s: %w", id, err)
|
||||
}
|
||||
pages = append(pages, page)
|
||||
}
|
||||
|
||||
return pages, nil
|
||||
}
|
||||
|
||||
func Read(id string) (*Page, error) {
|
||||
data, err := os.ReadFile("data/content/" + id + ".md")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading /data/content/%s.md: %w", id, err)
|
||||
}
|
||||
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithExtensions(extension.GFM, meta.New()),
|
||||
)
|
||||
context := parser.NewContext()
|
||||
var buf bytes.Buffer
|
||||
if err := markdown.Convert(data, &buf, parser.WithContext(context)); err != nil {
|
||||
return nil, fmt.Errorf("error converting markdown: %w", err)
|
||||
}
|
||||
|
||||
metaData := meta.Get(context)
|
||||
page := &Page{
|
||||
Content: buf.String(),
|
||||
Id: id,
|
||||
Title: id,
|
||||
}
|
||||
if title, ok := metaData["title"]; ok {
|
||||
page.Title = title.(string)
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
25
internal/web/content.go
Normal file
25
internal/web/content.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/amir20/dozzle/internal/content"
|
||||
"github.com/go-chi/chi/v5"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (h *handler) staticContent(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
content, err := content.Read(id)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
log.Warnf("error reading content: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(content); err != nil {
|
||||
log.Errorf("json encoding error while streaming %v", err.Error())
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"path"
|
||||
|
||||
"github.com/amir20/dozzle/internal/auth"
|
||||
"github.com/amir20/dozzle/internal/content"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/amir20/dozzle/internal/profile"
|
||||
|
||||
@@ -52,6 +53,16 @@ func (h *handler) executeTemplate(w http.ResponseWriter, req *http.Request) {
|
||||
"authProvider": h.config.AuthProvider,
|
||||
}
|
||||
|
||||
pages, err := content.ReadAll()
|
||||
if err != nil {
|
||||
log.Errorf("error reading content: %v", err)
|
||||
} else if len(pages) > 0 {
|
||||
for _, page := range pages {
|
||||
page.Content = ""
|
||||
}
|
||||
config["pages"] = pages
|
||||
}
|
||||
|
||||
user := auth.UserFromContext(req.Context())
|
||||
if user != nil {
|
||||
if settings, err := profile.LoadUserSettings(user); err == nil {
|
||||
@@ -90,8 +101,13 @@ func (h *handler) executeTemplate(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
tmpl, err := template.New("index.html").Funcs(template.FuncMap{
|
||||
"marshal": func(v interface{}) template.JS {
|
||||
a, _ := json.Marshal(v)
|
||||
return template.JS(a)
|
||||
var p []byte
|
||||
if h.config.Dev {
|
||||
p, _ = json.MarshalIndent(v, "", " ")
|
||||
} else {
|
||||
p, _ = json.Marshal(v)
|
||||
}
|
||||
return template.JS(p)
|
||||
},
|
||||
}).Parse(string(bytes))
|
||||
if err != nil {
|
||||
|
||||
@@ -97,6 +97,7 @@ func createRouter(h *handler) *chi.Mux {
|
||||
r.Get("/api/logs/{host}/{id}", h.fetchLogsBetweenDates)
|
||||
r.Get("/api/events/stream", h.streamEvents)
|
||||
r.Put("/api/profile/settings", h.saveSettings)
|
||||
r.Get("/api/content/{id}", h.staticContent)
|
||||
r.Get("/logout", h.clearSession) // TODO remove this
|
||||
r.Get("/version", h.version)
|
||||
})
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"@iconify-json/octicon": "^1.1.49",
|
||||
"@iconify-json/ph": "^1.1.6",
|
||||
"@intlify/unplugin-vue-i18n": "^1.5.0",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@vueuse/components": "^10.5.0",
|
||||
"@vueuse/core": "^10.5.0",
|
||||
"@vueuse/integrations": "^10.5.0",
|
||||
|
||||
35
pnpm-lock.yaml
generated
35
pnpm-lock.yaml
generated
@@ -26,6 +26,9 @@ dependencies:
|
||||
'@intlify/unplugin-vue-i18n':
|
||||
specifier: ^1.5.0
|
||||
version: 1.5.0(vue-i18n@9.6.5)
|
||||
'@tailwindcss/typography':
|
||||
specifier: ^0.5.10
|
||||
version: 0.5.10(tailwindcss@3.3.5)
|
||||
'@vueuse/components':
|
||||
specifier: ^10.5.0
|
||||
version: 10.5.0(vue@3.3.7)
|
||||
@@ -898,6 +901,18 @@ packages:
|
||||
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
|
||||
dev: true
|
||||
|
||||
/@tailwindcss/typography@0.5.10(tailwindcss@3.3.5):
|
||||
resolution: {integrity: sha512-Pe8BuPJQJd3FfRnm6H0ulKIGoMEQS+Vq01R6M5aCrFB/ccR/shT+0kXLjouGC1gFLm9hopTFN+DMP0pfwRWzPw==}
|
||||
peerDependencies:
|
||||
tailwindcss: '>=3.0.0 || insiders'
|
||||
dependencies:
|
||||
lodash.castarray: 4.4.0
|
||||
lodash.isplainobject: 4.0.6
|
||||
lodash.merge: 4.6.2
|
||||
postcss-selector-parser: 6.0.10
|
||||
tailwindcss: 3.3.5(ts-node@10.9.1)
|
||||
dev: false
|
||||
|
||||
/@tootallnate/once@2.0.0:
|
||||
resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
|
||||
engines: {node: '>= 10'}
|
||||
@@ -3263,10 +3278,22 @@ packages:
|
||||
dependencies:
|
||||
p-locate: 5.0.0
|
||||
|
||||
/lodash.castarray@4.4.0:
|
||||
resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==}
|
||||
dev: false
|
||||
|
||||
/lodash.debounce@4.0.8:
|
||||
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
||||
dev: false
|
||||
|
||||
/lodash.isplainobject@4.0.6:
|
||||
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
||||
dev: false
|
||||
|
||||
/lodash.merge@4.6.2:
|
||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||
dev: false
|
||||
|
||||
/lodash@4.17.21:
|
||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||
|
||||
@@ -3738,6 +3765,14 @@ packages:
|
||||
postcss-selector-parser: 6.0.13
|
||||
dev: false
|
||||
|
||||
/postcss-selector-parser@6.0.10:
|
||||
resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==}
|
||||
engines: {node: '>=4'}
|
||||
dependencies:
|
||||
cssesc: 3.0.0
|
||||
util-deprecate: 1.0.2
|
||||
dev: false
|
||||
|
||||
/postcss-selector-parser@6.0.13:
|
||||
resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
import DaisyUI from "daisyui";
|
||||
import Typography from "@tailwindcss/typography";
|
||||
|
||||
export default {
|
||||
content: ["./assets/**/*.{vue,js,ts}", "./public/index.html"],
|
||||
@@ -26,7 +27,7 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [DaisyUI],
|
||||
plugins: [DaisyUI, Typography],
|
||||
daisyui: {
|
||||
themes: [],
|
||||
base: false,
|
||||
|
||||
Reference in New Issue
Block a user