From 269fde97d65ee5b81f5d75c578ab0f423fe9cb5d Mon Sep 17 00:00:00 2001 From: Igor Shishkin Date: Thu, 2 Jan 2025 06:15:29 +0300 Subject: [PATCH] Switch publisher to Bootstrap based templates & add dark theme (#302) Signed-off-by: Igor Shishkin --- cmd/publisher/main.go | 8 +- dockerfiles/Dockerfile.publisher | 37 ++++++- publisher/presenter/html/handlers.go | 98 +++++++++++-------- publisher/presenter/html/handlers_test.go | 5 +- publisher/presenter/html/pagination.go | 70 +++++++++++++ publisher/presenter/html/pagination_test.go | 27 +++++ .../presenter/html/static/scripts/index.ts | 26 +++++ .../presenter/html/static/styles/index.css | 6 ++ publisher/presenter/html/templates/404.html | 19 ++-- publisher/presenter/html/templates/5xx.html | 19 ++-- .../presenter/html/templates/_common.html | 72 ++++++++++---- .../html/templates/container-list.html | 5 +- .../html/templates/namespace-list.html | 2 + .../presenter/html/templates/object-list.html | 6 +- .../html/templates/version-list.html | 5 +- .../presenter/html/testdata/404.html.sample | 19 ++-- .../presenter/html/testdata/5xx.html.sample | 19 ++-- .../html/testdata/containers.html.sample | 82 +++++++++++++--- .../html/testdata/namespaces.html.sample | 43 ++++++-- .../html/testdata/objects.html.sample | 83 +++++++++++++--- .../html/testdata/versions.html.sample | 82 +++++++++++++--- 21 files changed, 595 insertions(+), 138 deletions(-) create mode 100644 publisher/presenter/html/pagination.go create mode 100644 publisher/presenter/html/pagination_test.go create mode 100644 publisher/presenter/html/static/scripts/index.ts create mode 100644 publisher/presenter/html/static/styles/index.css diff --git a/cmd/publisher/main.go b/cmd/publisher/main.go index c7c6864..b6478f7 100644 --- a/cmd/publisher/main.go +++ b/cmd/publisher/main.go @@ -62,6 +62,9 @@ type config struct { VersionsPerPage uint64 `envconfig:"VERSIONS_PER_PAGE" default:"50"` ObjectsPerPage uint64 `envconfig:"OBJECTS_PER_PAGE" default:"50"` ContainersPerPage uint64 `envconfig:"CONTAINERS_PER_PAGE" default:"50"` + + MaxPagesInPagination uint64 `envconfig:"MAX_PAGES_IN_PAGINATION" default:"5"` + DefaultTheme htmlPresenter.Theme `envconfig:"DEFAULT_THEME" default:"dark"` } func main() { @@ -126,7 +129,10 @@ func main() { publisherSvc := service.NewPublisher(repo, blobRepo, cfg.VersionsPerPage, cfg.ObjectsPerPage, cfg.ContainersPerPage) - p := htmlPresenter.New(publisherSvc, cfg.HTMLTemplateDir, cfg.StaticDir, cfg.BLOBS3PreserveSchemeOnRedirect) + p := htmlPresenter.New(publisherSvc, cfg.HTMLTemplateDir, cfg.StaticDir, cfg.BLOBS3PreserveSchemeOnRedirect, htmlPresenter.DisplayConfig{ + DefaultTheme: cfg.DefaultTheme, + MaxPagesInPagination: cfg.MaxPagesInPagination, + }) p.Register(e) g.Go(func() error { diff --git a/dockerfiles/Dockerfile.publisher b/dockerfiles/Dockerfile.publisher index 098f98c..7599125 100644 --- a/dockerfiles/Dockerfile.publisher +++ b/dockerfiles/Dockerfile.publisher @@ -3,14 +3,45 @@ FROM alpine:3.20.3 AS certificates RUN apk add --update --no-cache \ ca-certificates=20240705-r0 -FROM scratch +FROM index.docker.io/library/node:22.12.0 AS depsbuilder + +RUN mkdir /src +WORKDIR /src +RUN npm install \ + bootstrap@5.3.3 \ + bootstrap-icons@1.11.3 + +FROM index.docker.io/library/node:22.12.0 AS tsbuilder + +RUN mkdir /src +WORKDIR /src +RUN npm install -g \ + typescript@5.7.2 +COPY publisher/presenter/html/static/scripts /src/scripts +RUN tsc scripts/index.ts --strict --removeComments --outDir /build + +FROM ubuntu:24.04 + +RUN mkdir -p \ + /static/archived/scripts \ + /static/archived/styles + COPY dockerfiles/rootfs/etc/passwd /etc/passwd COPY dockerfiles/rootfs/etc/group /etc/group -COPY --from=certificates /etc/ssl/cert.pem /etc/ssl/cert.pem +COPY --from=certificates --chown=root:root --chmod=0644 /etc/ssl/cert.pem /etc/ssl/cert.pem COPY --chmod=0755 --chown=root:root dist/archived-publisher_linux_amd64_v3/archived-publisher /archived-publisher -COPY --chmod=0644 --chown=root:root publisher/presenter/html/templates /templates +COPY --chmod=0755 --chown=root:root publisher/presenter/html/templates /templates +COPY --chmod=0755 --chown=root:root publisher/presenter/html/static /static/archived + +COPY --from=tsbuilder --chown=root:root --chmod=0644 /build /static/archived/scripts + +COPY --from=depsbuilder --chown=root:root /src/node_modules/bootstrap/dist /static/bootstrap +COPY --from=depsbuilder --chown=root:root /src/node_modules/@popperjs/core/dist/umd /static/popperjs +COPY --from=depsbuilder --chown=root:root /src/node_modules/bootstrap-icons/bootstrap-icons.svg /static/bootstrap-icons/bootstrap-icons.svg +COPY --from=depsbuilder --chown=root:root /src/node_modules/bootstrap-icons/font /static/bootstrap-icons/font +COPY --from=depsbuilder --chown=root:root /src/node_modules/bootstrap-icons/icons /static/bootstrap-icons/icons ENV HTML_TEMPLATE_DIR=/templates ENV STATIC_DIR=/static diff --git a/publisher/presenter/html/handlers.go b/publisher/presenter/html/handlers.go index 9a6d3e6..67fbea2 100644 --- a/publisher/presenter/html/handlers.go +++ b/publisher/presenter/html/handlers.go @@ -29,19 +29,33 @@ type Handlers interface { Register(e *echo.Echo) } +type Theme string + +const ( + ThemeDark Theme = "dark" + ThemeLight Theme = "light" +) + +type DisplayConfig struct { + DefaultTheme Theme + MaxPagesInPagination uint64 +} + type handlers struct { svc service.Publisher staticDir string templateDir string preserveSchemeOnRedirect bool + dc DisplayConfig } -func New(svc service.Publisher, templateDir, staticDir string, preserveSchemeOnRedirect bool) Handlers { +func New(svc service.Publisher, templateDir, staticDir string, preserveSchemeOnRedirect bool, dc DisplayConfig) Handlers { return &handlers{ svc: svc, staticDir: staticDir, templateDir: templateDir, preserveSchemeOnRedirect: preserveSchemeOnRedirect, + dc: dc, } } @@ -52,13 +66,15 @@ func (h *handlers) NamespaceIndex(c echo.Context) error { } type data struct { - Title string - Namespaces []string + Title string + DefaultTheme Theme + Namespaces []string } return c.Render(http.StatusOK, "namespace-list.html", &data{ - Title: "Namespace index", - Namespaces: namespaces, + Title: "Namespace index", + DefaultTheme: h.dc.DefaultTheme, + Namespaces: namespaces, }) } @@ -86,19 +102,19 @@ func (h *handlers) ContainerIndex(c echo.Context) error { } type data struct { - Title string - CurrentPage uint64 - PagesCount uint64 - Namespace string - Containers []models.Container + Title string + DefaultTheme Theme + Pagination Pagination + Namespace string + Containers []models.Container } return c.Render(http.StatusOK, "container-list.html", &data{ - Title: fmt.Sprintf("Container index (%s)", namespace), - CurrentPage: page, - PagesCount: pagesCount, - Namespace: namespace, - Containers: containers, + Title: fmt.Sprintf("Container index (%s)", namespace), + DefaultTheme: h.dc.DefaultTheme, + Pagination: NewPagination(pagesCount, page, h.dc.MaxPagesInPagination, fmt.Sprintf("/%s/", namespace)), + Namespace: namespace, + Containers: containers, }) } @@ -127,21 +143,21 @@ func (h *handlers) VersionIndex(c echo.Context) error { } type data struct { - Title string - CurrentPage uint64 - PagesCount uint64 - Namespace string - Container string - Versions []models.Version + Title string + DefaultTheme Theme + Pagination Pagination + Namespace string + Container string + Versions []models.Version } return c.Render(http.StatusOK, "version-list.html", &data{ - Title: fmt.Sprintf("Version index (%s/%s)", namespace, container), - CurrentPage: page, - PagesCount: pagesCount, - Namespace: namespace, - Container: container, - Versions: versions, + Title: fmt.Sprintf("Version index (%s/%s)", namespace, container), + DefaultTheme: h.dc.DefaultTheme, + Pagination: NewPagination(pagesCount, page, h.dc.MaxPagesInPagination, fmt.Sprintf("/%s/%s/", namespace, container)), + Namespace: namespace, + Container: container, + Versions: versions, }) } @@ -171,22 +187,22 @@ func (h *handlers) ObjectIndex(c echo.Context) error { } type data struct { - Title string - CurrentPage uint64 - PagesCount uint64 - Namespace string - Container string - Version string - Objects []string + Title string + DefaultTheme Theme + Pagination Pagination + Namespace string + Container string + Version string + Objects []string } return c.Render(http.StatusOK, "object-list.html", &data{ - Title: fmt.Sprintf("Object index (%s/%s/%s)", namespace, container, version), - CurrentPage: page, - PagesCount: pagesCount, - Namespace: namespace, - Container: container, - Version: version, - Objects: objects, + Title: fmt.Sprintf("Object index (%s/%s/%s)", namespace, container, version), + DefaultTheme: h.dc.DefaultTheme, + Pagination: NewPagination(pagesCount, page, h.dc.MaxPagesInPagination, fmt.Sprintf("/%s/%s/%s/", namespace, container, version)), + Namespace: namespace, + Container: container, + Version: version, + Objects: objects, }) } diff --git a/publisher/presenter/html/handlers_test.go b/publisher/presenter/html/handlers_test.go index a955e8a..9556696 100644 --- a/publisher/presenter/html/handlers_test.go +++ b/publisher/presenter/html/handlers_test.go @@ -173,7 +173,10 @@ func (s *handlersTestSuite) SetupTest() { s.serviceMock = service.NewMock() - s.handlers = New(s.serviceMock, "templates", "static", true) + s.handlers = New(s.serviceMock, "templates", "static", true, DisplayConfig{ + DefaultTheme: ThemeDark, + MaxPagesInPagination: 5, + }) s.handlers.Register(e) s.srv = httptest.NewServer(e) diff --git a/publisher/presenter/html/pagination.go b/publisher/presenter/html/pagination.go new file mode 100644 index 0000000..7554ae9 --- /dev/null +++ b/publisher/presenter/html/pagination.go @@ -0,0 +1,70 @@ +package html + +import "strconv" + +type Pagination struct { + HasPrevious bool + PreviousPageNum uint64 + PreviousPageURL string + Pages []Page + HasNext bool + NextPageNum uint64 + NextPageURL string +} + +type Page struct { + IsCurrent bool + Number uint64 + URL string +} + +func NewPagination(total, current, maxPages uint64, pageURL string) Pagination { + pages := []Page{} + max := current + maxPages + if current+maxPages > total { + max = total + } + for i := current; i <= max; i++ { + isCurrent := false + if i == current { + isCurrent = true + } + pages = append(pages, Page{ + IsCurrent: isCurrent, + Number: i, + URL: pageURL + "?page=" + strconv.FormatUint(i, 10), + }) + } + + var ( + hasPrevious bool + previousPageNum uint64 + previousPageURL string + ) + if current > 1 { + hasPrevious = true + previousPageNum = current - 1 + previousPageURL = pageURL + "?page=" + strconv.FormatUint(previousPageNum, 10) + } + + var ( + hasNext bool + nextPageNum uint64 + nextPageURL string + ) + if max < total { + hasNext = true + nextPageNum = current + maxPages + 1 + nextPageURL = pageURL + "?page=" + strconv.FormatUint(nextPageNum, 10) + } + + return Pagination{ + HasPrevious: hasPrevious, + PreviousPageNum: previousPageNum, + PreviousPageURL: previousPageURL, + Pages: pages, + HasNext: hasNext, + NextPageNum: nextPageNum, + NextPageURL: nextPageURL, + } +} diff --git a/publisher/presenter/html/pagination_test.go b/publisher/presenter/html/pagination_test.go new file mode 100644 index 0000000..7f0a979 --- /dev/null +++ b/publisher/presenter/html/pagination_test.go @@ -0,0 +1,27 @@ +package html + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPagination(t *testing.T) { + r := require.New(t) + + pagination := NewPagination(305, 10, 3, "/some/page") + r.Equal(Pagination{ + HasPrevious: true, + PreviousPageNum: 9, + PreviousPageURL: "/some/page?page=9", + Pages: []Page{ + {IsCurrent: true, URL: "/some/page?page=10", Number: 10}, + {IsCurrent: false, URL: "/some/page?page=11", Number: 11}, + {IsCurrent: false, URL: "/some/page?page=12", Number: 12}, + {IsCurrent: false, URL: "/some/page?page=13", Number: 13}, + }, + HasNext: true, + NextPageNum: 14, + NextPageURL: "/some/page?page=14", + }, pagination) +} diff --git a/publisher/presenter/html/static/scripts/index.ts b/publisher/presenter/html/static/scripts/index.ts new file mode 100644 index 0000000..224d341 --- /dev/null +++ b/publisher/presenter/html/static/scripts/index.ts @@ -0,0 +1,26 @@ +window.addEventListener('load', () => { + const currentSwitch = localStorage.getItem('lightSwitch')?.toString() + const htmlTag = document.getElementById('html'); + const lightSwitch = document.getElementById('lightSwitch'); + + if (currentSwitch) { + htmlTag?.setAttribute('data-bs-theme', currentSwitch); + } + + lightSwitch?.addEventListener('click', () => { + if (document.getElementById('html')?.getAttribute('data-bs-theme')?.toString() == "dark") { + htmlTag?.setAttribute('data-bs-theme', 'light'); + lightSwitch?.classList.remove('dark'); + lightSwitch?.classList.add('light'); + + localStorage.setItem('lightSwitch', 'light'); + return + } + htmlTag?.setAttribute('data-bs-theme', 'dark'); + lightSwitch?.classList.remove('light'); + lightSwitch?.classList.add('dark'); + + localStorage.setItem('lightSwitch', 'dark'); + return + }); +}); diff --git a/publisher/presenter/html/static/styles/index.css b/publisher/presenter/html/static/styles/index.css new file mode 100644 index 0000000..0c07f94 --- /dev/null +++ b/publisher/presenter/html/static/styles/index.css @@ -0,0 +1,6 @@ +.container { + margin: 15px; +} +nav.pagination { + margin-left: 45px; +} diff --git a/publisher/presenter/html/templates/404.html b/publisher/presenter/html/templates/404.html index 95bb94d..20ed11b 100644 --- a/publisher/presenter/html/templates/404.html +++ b/publisher/presenter/html/templates/404.html @@ -1,14 +1,21 @@ - - Resource not found - - + + + Resource not found + + + +

404 Not Found

There is not the resource you are looking for

-
+
+
+
Powered by archived - +
+ + diff --git a/publisher/presenter/html/templates/5xx.html b/publisher/presenter/html/templates/5xx.html index f6f07fc..06f913e 100644 --- a/publisher/presenter/html/templates/5xx.html +++ b/publisher/presenter/html/templates/5xx.html @@ -1,14 +1,21 @@ - - {{ .Message }} - - + + + {{ .Message }} + + + +

{{ .Code }} {{ .Message }}

Something went wrong.....

-
+
+
+
Powered by archived - +
+ + diff --git a/publisher/presenter/html/templates/_common.html b/publisher/presenter/html/templates/_common.html index e77e894..b747c2f 100644 --- a/publisher/presenter/html/templates/_common.html +++ b/publisher/presenter/html/templates/_common.html @@ -1,30 +1,62 @@ {{ define "header" }} - - - {{ .Title }} - - + + + + + + + + + + + + {{ .Title }} + + + +

{{ .Title }}

-
-{{ end }} +
+ +
+ {{ end }} -{{ define "footer" }} -
+ {{ define "footer" }} +
+
Powered by archived - +
+ + {{ end }} {{ define "pagination" }} -

-{{- $current := .CurrentPage }} -{{- $total := .PagesCount }} -{{- if gt $current 1 }} - « -{{- end }} -{{- if lt $current $total }} - » -{{- end }} -

+ {{ end }} diff --git a/publisher/presenter/html/templates/container-list.html b/publisher/presenter/html/templates/container-list.html index 4b716a7..83337c6 100644 --- a/publisher/presenter/html/templates/container-list.html +++ b/publisher/presenter/html/templates/container-list.html @@ -1,11 +1,14 @@ {{ $namespace := .Namespace }} {{ template "header" . }} +
..
{{- range $container := .Containers }} {{ $container.Name }}/
{{- else }}

No containers found

{{- end }} +
+ +{{ template "pagination" .Pagination }} -{{ template "pagination" . }} {{ template "footer" }} diff --git a/publisher/presenter/html/templates/namespace-list.html b/publisher/presenter/html/templates/namespace-list.html index a81ff1d..1068cc5 100644 --- a/publisher/presenter/html/templates/namespace-list.html +++ b/publisher/presenter/html/templates/namespace-list.html @@ -1,7 +1,9 @@ {{ template "header" . }} +
{{- range $namespace := .Namespaces }} {{ $namespace }}/
{{- else }}

No namespaces found

{{- end }} +
{{ template "footer" }} diff --git a/publisher/presenter/html/templates/object-list.html b/publisher/presenter/html/templates/object-list.html index 33569e1..7502d75 100644 --- a/publisher/presenter/html/templates/object-list.html +++ b/publisher/presenter/html/templates/object-list.html @@ -2,11 +2,15 @@ {{ $container := .Container }} {{ $version := .Version }} {{ template "header" . }} +
..
{{- range $object := .Objects }} {{ $object }}
{{- else }}

No objects found

{{- end }} -{{ template "pagination" . }} +
+ +{{ template "pagination" .Pagination }} + {{ template "footer" }} diff --git a/publisher/presenter/html/templates/version-list.html b/publisher/presenter/html/templates/version-list.html index 8012f0f..a8bdeec 100644 --- a/publisher/presenter/html/templates/version-list.html +++ b/publisher/presenter/html/templates/version-list.html @@ -1,12 +1,15 @@ {{ $namespace := .Namespace }} {{ $container := .Container }} {{ template "header" . }} +
..
{{- range $version := .Versions }} {{ $version.Name }}/
{{- else }}

No versions found

{{- end }} +
+ +{{ template "pagination" .Pagination }} -{{ template "pagination" . }} {{ template "footer" }} diff --git a/publisher/presenter/html/testdata/404.html.sample b/publisher/presenter/html/testdata/404.html.sample index 95bb94d..20ed11b 100644 --- a/publisher/presenter/html/testdata/404.html.sample +++ b/publisher/presenter/html/testdata/404.html.sample @@ -1,14 +1,21 @@ - - Resource not found - - + + + Resource not found + + + +

404 Not Found

There is not the resource you are looking for

-
+
+
+
Powered by archived - +
+ + diff --git a/publisher/presenter/html/testdata/5xx.html.sample b/publisher/presenter/html/testdata/5xx.html.sample index 394cd86..fdce9ed 100644 --- a/publisher/presenter/html/testdata/5xx.html.sample +++ b/publisher/presenter/html/testdata/5xx.html.sample @@ -1,14 +1,21 @@ - - Internal Server Error - - + + + Internal Server Error + + + +

500 Internal Server Error

Something went wrong.....

-
+
+
+
Powered by archived - +
+ + diff --git a/publisher/presenter/html/testdata/containers.html.sample b/publisher/presenter/html/testdata/containers.html.sample index e1eddcc..4ac2cf3 100644 --- a/publisher/presenter/html/testdata/containers.html.sample +++ b/publisher/presenter/html/testdata/containers.html.sample @@ -1,25 +1,83 @@ - - - Container index (default) - - -

Container index (default)

-
+ + + + + + + + + + + + Container index (default) + + +
+

Container index (default)

+
+ +
+ + -

- » -

+ -
+ +
+
Powered by archived - +
+ + diff --git a/publisher/presenter/html/testdata/namespaces.html.sample b/publisher/presenter/html/testdata/namespaces.html.sample index 9342da8..f2a243b 100644 --- a/publisher/presenter/html/testdata/namespaces.html.sample +++ b/publisher/presenter/html/testdata/namespaces.html.sample @@ -1,18 +1,43 @@ - - - Namespace index - - -

Namespace index

-
+ + + + + + + + + + + + Namespace index + + +
+

Namespace index

+
+ +
+ + -
+
+
Powered by archived - +
+ + diff --git a/publisher/presenter/html/testdata/objects.html.sample b/publisher/presenter/html/testdata/objects.html.sample index b151aef..c659899 100644 --- a/publisher/presenter/html/testdata/objects.html.sample +++ b/publisher/presenter/html/testdata/objects.html.sample @@ -3,24 +3,83 @@ - - - Object index (default/test-container-1/20241011121314) - - -

Object index (default/test-container-1/20241011121314)

-
+ + + + + + + + + + + + Object index (default/test-container-1/20241011121314) + + +
+

Object index (default/test-container-1/20241011121314)

+
+ +
+ + -

- » -

+ -
+ + +
+
Powered by archived - +
+ + diff --git a/publisher/presenter/html/testdata/versions.html.sample b/publisher/presenter/html/testdata/versions.html.sample index 44c7ad0..db09e39 100644 --- a/publisher/presenter/html/testdata/versions.html.sample +++ b/publisher/presenter/html/testdata/versions.html.sample @@ -2,25 +2,83 @@ - - - Version index (default/test-container-1) - - -

Version index (default/test-container-1)

-
+ + + + + + + + + + + + Version index (default/test-container-1) + + +
+

Version index (default/test-container-1)

+
+ +
+ + -

- » -

+ -
+ +
+
Powered by archived - +
+ +