1
0
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:
Amir Raminfar
2023-11-06 12:39:11 -08:00
committed by GitHub
parent 1e1c64d6e7
commit 32de313c27
17 changed files with 228 additions and 24 deletions

View File

@@ -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']

View 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>

View File

@@ -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">

View File

@@ -118,4 +118,8 @@
--nc: var(--pc);
@apply active;
}
.link-primary {
@apply underline-offset-4 hover:underline;
}
}

View 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>

View File

@@ -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) {

View File

@@ -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>

View File

@@ -18,6 +18,7 @@ interface Config {
avatar: string;
};
serverSettings?: Settings;
pages?: { id: string; title: string }[];
}
const pageConfig = JSON.parse(text);

3
go.mod
View File

@@ -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
View File

@@ -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
View 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
View 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())
}
}

View File

@@ -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 {

View File

@@ -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)
})

View File

@@ -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
View File

@@ -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'}

View File

@@ -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,