Skip to content

Commit a027c32

Browse files
authored
Merge pull request #119 from lsst-sqre/u/jsickcodes/builtin-dashboard
Implement dashboard generation from built-in templates
2 parents c75ff65 + 2d8dc8b commit a027c32

20 files changed

+991
-24
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ pgdb/
6969

7070
integration_tests/ltd_keeper_doc_examples.txt
7171

72+
# Dashboard development
73+
dashboard_dev
74+
7275
# Kubernetes deployment
7376
kubernetes/cloudsql-secrets.yaml
7477
kubernetes/keeper-secrets.yaml

keeper/dashboard/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Domain for edition dashboards."""

keeper/dashboard/context.py

+196
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
"""Generate Jinja template rendering context from domain models."""
2+
3+
from __future__ import annotations
4+
5+
from collections import UserList
6+
from dataclasses import dataclass
7+
from datetime import datetime
8+
from typing import List, Optional, Sequence
9+
10+
from keeper.models import Build, Edition, EditionKind, Product
11+
12+
13+
@dataclass
14+
class ProjectContext:
15+
"""Template context model for a project."""
16+
17+
title: str
18+
"""Project title."""
19+
20+
source_repo_url: str
21+
"""Url of the associated GitHub repository."""
22+
23+
url: str
24+
"""Root URL where this project is published."""
25+
26+
@classmethod
27+
def from_product(cls, product: Product) -> ProjectContext:
28+
return cls(
29+
title=product.title,
30+
source_repo_url=product.doc_repo,
31+
url=product.published_url,
32+
)
33+
34+
35+
@dataclass
36+
class EditionContext:
37+
"""Template context model for an edition."""
38+
39+
title: str
40+
"""Human-readable label for this edition."""
41+
42+
url: str
43+
"""URL where this edition is published."""
44+
45+
date_updated: datetime
46+
"""Date when this edition was last updated."""
47+
48+
kind: EditionKind
49+
"""The edition's kind."""
50+
51+
slug: str
52+
"""The edition's slug."""
53+
54+
git_ref: Optional[str]
55+
"""The git ref that this edition tracks."""
56+
57+
github_url: Optional[str]
58+
"""URL to this git ref on GitHub."""
59+
60+
@classmethod
61+
def from_edition(
62+
cls, edition: Edition, product: Product
63+
) -> EditionContext:
64+
if edition.tracked_ref and product.doc_repo:
65+
repo_url = product.doc_repo.rstrip("/")
66+
if repo_url[-4:] == ".git":
67+
repo_url = repo_url[:-4]
68+
github_url = f"{repo_url}/tree/{edition.tracked_ref}"
69+
else:
70+
github_url = None
71+
72+
return cls(
73+
title=edition.title,
74+
url=edition.published_url,
75+
date_updated=edition.date_rebuilt,
76+
kind=edition.kind,
77+
slug=edition.slug,
78+
git_ref=edition.tracked_ref,
79+
github_url=github_url,
80+
)
81+
82+
83+
class EditionContextList(UserList):
84+
def __init__(self, contexts: Sequence[EditionContext]) -> None:
85+
self.data: List[EditionContext] = list(contexts)
86+
self.data.sort(key=lambda x: x.date_updated)
87+
88+
@property
89+
def main_edition(self) -> EditionContext:
90+
"""The main (current) edition."""
91+
for edition in self.data:
92+
if edition.slug == "__main":
93+
return edition
94+
raise ValueError("No __main edition found")
95+
96+
@property
97+
def has_releases(self) -> bool:
98+
return len(self.releases) > 0
99+
100+
@property
101+
def releases(self) -> List[EditionContext]:
102+
"""All editions tagged as releases."""
103+
release_kinds = (
104+
EditionKind.release,
105+
EditionKind.major,
106+
EditionKind.minor,
107+
)
108+
release_items = [
109+
e
110+
for e in self.data
111+
if (e.kind in release_kinds and e.slug != "__main")
112+
]
113+
sorted_items = sorted(
114+
release_items, key=lambda x: x.slug, reverse=True
115+
)
116+
return sorted_items
117+
118+
@property
119+
def has_drafts(self) -> bool:
120+
return len(self.drafts) > 0
121+
122+
@property
123+
def drafts(self) -> List[EditionContext]:
124+
"""All editions tagged as drafts."""
125+
draft_items = [
126+
e
127+
for e in self.data
128+
if (e.kind == EditionKind.draft and e.slug != "__main")
129+
]
130+
return sorted(draft_items, key=lambda x: x.date_updated, reverse=True)
131+
132+
133+
@dataclass
134+
class BuildContext:
135+
"""Template context model for a build."""
136+
137+
slug: str
138+
"""The URL slug for this build."""
139+
140+
url: str
141+
"""The URL for this build."""
142+
143+
git_ref: Optional[str]
144+
"""The git ref associated with this build (if appropriate."""
145+
146+
date: datetime
147+
"""Date when the build was uploaded."""
148+
149+
@classmethod
150+
def from_build(cls, build: Build) -> BuildContext:
151+
return cls(
152+
slug=build.slug,
153+
url=build.published_url,
154+
git_ref=build.git_ref,
155+
date=build.date_created,
156+
)
157+
158+
159+
class BuildContextList(UserList):
160+
def __init__(self, contexts: Sequence[BuildContext]) -> None:
161+
self.data: List[BuildContext] = list(contexts)
162+
self.data.sort(key=lambda x: x.date)
163+
164+
165+
@dataclass
166+
class Context:
167+
"""A class that creates Jinja template rendering context from
168+
domain models.
169+
"""
170+
171+
project_context: ProjectContext
172+
173+
edition_contexts: EditionContextList
174+
175+
build_contexts: BuildContextList
176+
177+
@classmethod
178+
def create(cls, product: Product) -> Context:
179+
project_context = ProjectContext.from_product(product)
180+
181+
edition_contexts: EditionContextList = EditionContextList(
182+
[
183+
EditionContext.from_edition(edition=edition, product=product)
184+
for edition in product.editions
185+
]
186+
)
187+
188+
build_contexts: BuildContextList = BuildContextList(
189+
[BuildContext.from_build(build) for build in product.builds]
190+
)
191+
192+
return cls(
193+
project_context=project_context,
194+
edition_contexts=edition_contexts,
195+
build_contexts=build_contexts,
196+
)

keeper/dashboard/jinjafilters.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""Filters for Jinja2 templates."""
2+
3+
from __future__ import annotations
4+
5+
__all__ = ["filter_simple_date"]
6+
7+
from datetime import datetime
8+
9+
10+
def filter_simple_date(value: datetime) -> str:
11+
"""Filter a `datetime.datetime` into a 'YYYY-MM-DD' string."""
12+
return value.strftime("%Y-%m-%d")

keeper/dashboard/static/app.css

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/* Resets */
2+
*,
3+
*::before,
4+
*::after {
5+
box-sizing: border-box;
6+
}
7+
8+
* {
9+
margin: 0;
10+
}
11+
12+
html,
13+
body {
14+
height: 100%;
15+
}
16+
17+
body {
18+
line-height: 1.5;
19+
-webkit-font-smoothing: antialiased;
20+
}
21+
22+
img,
23+
picture,
24+
video,
25+
canvas,
26+
svg {
27+
display: block;
28+
max-width: 100%;
29+
}
30+
31+
input,
32+
button,
33+
textarea,
34+
select {
35+
font: inherit;
36+
}
37+
38+
p,
39+
h1,
40+
h2,
41+
h3,
42+
h4,
43+
h5,
44+
h6 {
45+
overflow-wrap: break-word;
46+
}
47+
48+
/* System font stack */
49+
body {
50+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
51+
Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
52+
}
53+
54+
main {
55+
/* max-width: 16rem; */
56+
width: 100vw;
57+
margin: 0 auto;
58+
padding: 1rem;
59+
}
60+
61+
@media (min-width: 62rem) {
62+
main {
63+
width: 62rem;
64+
}
65+
}
66+
67+
.main-edition-section {
68+
margin-top: 1rem;
69+
}
70+
71+
.main-edition-section__url {
72+
font-size: 1.2rem;
73+
margin-bottom: 0.5rem;
74+
}
75+
76+
.version-section {
77+
margin-top: 2rem;
78+
}
79+
80+
.version-section__listing {
81+
list-style: none;
82+
padding-left: 0;
83+
}
84+
85+
.version-section__listing > li {
86+
margin-top: 1rem;
87+
}
88+
89+
.dashboard-item-metadata {
90+
list-style: none;
91+
display: flex;
92+
flex-direction: row;
93+
align-items: baseline;
94+
gap: 1.2rem;
95+
padding-left: 0;
96+
}

keeper/dashboard/template/base.jinja

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<!doctype html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="utf-8">
6+
<title>{% block page_title %}{% endblock page_title %}</title>
7+
<meta name="description" content="{% block page_description %}{% endblock page_description %}">
8+
<meta name="viewport" content="width=device-width, initial-scale=1">
9+
<link rel="stylesheet" href="{{ asset_dir }}/app.css">
10+
</head>
11+
12+
<body>
13+
{% block body %}
14+
{% endblock body %}
15+
</body>
16+
17+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{% extends "base.jinja" %}
2+
3+
{% block page_title %}{{ project.title }} builds{% endblock page_title %}
4+
{% block page_description %}Find documentation builds.{% endblock page_description %}
5+
6+
{% block body %}
7+
<main>
8+
<header>
9+
<h1>{{ project.title }} builds</h1>
10+
</header>
11+
<section>
12+
<header>
13+
<h2>Builds</h2>
14+
</header>
15+
</section>
16+
</main>
17+
{% endblock body %}

0 commit comments

Comments
 (0)