This is an automated email from the ASF dual-hosted git repository.
mfordjody pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/dubbo-kubernetes.git
The following commit(s) were added to refs/heads/master by this push:
new abe53dd1 Update moviereview sample (#917)
abe53dd1 is described below
commit abe53dd1b78cdff66382134dccda88835e80ca88
Author: mfordjody <[email protected]>
AuthorDate: Thu Jun 18 10:35:15 2026 +0800
Update moviereview sample (#917)
* delete logo
* Update moviereview sample
---
README.md | 14 +-
samples/moviereview/src/details/main.go | 8 +
samples/moviereview/src/moviepage/app.py | 599 +++++++++++++++++----------
samples/moviereview/src/ratings/main.go | 4 +-
samples/moviereview/src/reviews-v2/app.py | 1 -
samples/moviereview/src/reviews-v3/server.js | 1 -
6 files changed, 386 insertions(+), 241 deletions(-)
diff --git a/README.md b/README.md
index 491abdb6..49018281 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,9 @@
# Apache Kdubbo
- <a href="https://pkg.go.dev/github.com/apache/dubbo-kubernetes">
- <img src="https://pkg.go.dev/badge/github.com/apache/dubbo-kubernetes.svg"
/>
- </a>
- <a href="https://goreportcard.com/report/github.com/apache/dubbo-kubernetes">
- <img
src="https://goreportcard.com/badge/github.com/apache/dubbo-kubernetes" />
- </a>
- <a href="https://codecov.io/gh/apache/dubbo-kubernetes">
- <img
src="https://codecov.io/gh/apache/dubbo-kubernetes/branch/master/graph/badge.svg"
/>
- </a>
- <img src="https://img.shields.io/badge/license-Apache--2.0-green.svg" />
+[](https://pkg.go.dev/github.com/apache/dubbo-kubernetes)
+[](https://goreportcard.com/report/github.com/apache/dubbo-kubernetes)
+[](https://codecov.io/gh/apache/dubbo-kubernetes)
+[](https://www.apache.org/licenses/LICENSE-2.0)
Dubbo inherent mesh implemented for the underlying cluster management platform
can directly receive policies from the control plane and obtain features such
as load balancing, service discovery, and observability without requiring a
sidecar proxy.
diff --git a/samples/moviereview/src/details/main.go
b/samples/moviereview/src/details/main.go
index 5fc787d4..2861d2a8 100644
--- a/samples/moviereview/src/details/main.go
+++ b/samples/moviereview/src/details/main.go
@@ -26,6 +26,10 @@ type movieDetails struct {
Year int `json:"year"`
Director string `json:"director"`
Genres []string `json:"genres"`
+ Runtime string `json:"runtime"`
+ Language string `json:"language"`
+ Region string `json:"region"`
+ MovieID string `json:"movieId"`
Summary string `json:"summary"`
}
@@ -43,6 +47,10 @@ func main() {
Year: 2019,
Director: "Deng Chao",
Genres: []string{"Drama", "Family"},
+ Runtime: "147 min",
+ Language: "Mandarin",
+ Region: "China",
+ MovieID: "movie-1",
Summary: "A father and son story used here as a small,
normal microservice domain.",
})
})
diff --git a/samples/moviereview/src/moviepage/app.py
b/samples/moviereview/src/moviepage/app.py
index 19fc3ccf..eb8f05eb 100644
--- a/samples/moviereview/src/moviepage/app.py
+++ b/samples/moviereview/src/moviepage/app.py
@@ -17,21 +17,30 @@ import os
from html import escape
import requests
-from flask import Flask
+from flask import Flask, redirect, request, session, url_for
app = Flask(__name__)
+app.secret_key = os.getenv("MOVIEREVIEW_SESSION_SECRET",
"moviereview-session-secret")
DETAILS_URL = os.getenv("DETAILS_URL", "http://details:9080/details")
REVIEWS_URL = os.getenv("REVIEWS_URL", "http://reviews:9080/reviews")
-def get_json(url, fallback):
+def forward_headers():
+ headers = {}
+ user = session.get("user")
+ if user:
+ headers["end-user"] = user
+ return headers
+
+
+def get_json(url, fallback, headers=None):
try:
- response = requests.get(url, timeout=2)
+ response = requests.get(url, headers=headers or {}, timeout=2)
response.raise_for_status()
return response.json()
- except requests.RequestException as exc:
+ except (requests.RequestException, ValueError) as exc:
data = fallback.copy()
data["error"] = str(exc)
return data
@@ -42,31 +51,107 @@ def healthz():
return "ok\n"
[email protected]("/login")
+def login():
+ username = request.form.get("username", "").strip()
+ password = (request.form.get("passwd") or request.form.get("password") or
"").strip()
+ if username and password:
+ session["user"] = username[:64]
+ return redirect(request.referrer or url_for("index"))
+
+
[email protected]("/logout")
+def logout():
+ session.pop("user", None)
+ return redirect(request.referrer or url_for("index"))
+
+
@app.get("/")
def index():
- details = get_json(DETAILS_URL, {"title": "Unknown", "director": "-",
"year": "-", "genres": []})
- reviews = get_json(REVIEWS_URL, {"version": "unavailable", "items": [],
"rating": None})
+ headers = forward_headers()
+ user = session.get("user", "")
+ details = get_json(
+ DETAILS_URL,
+ {
+ "title": "Unknown",
+ "director": "-",
+ "year": "-",
+ "genres": [],
+ "runtime": "147 min",
+ "language": "Mandarin",
+ "region": "China",
+ "movieId": "movie-1",
+ },
+ headers,
+ )
+ reviews = get_json(REVIEWS_URL, {"version": "unavailable", "items": [],
"rating": None}, headers)
+ page_status = 200
title = escape(str(details.get("title", "Unknown")))
year = escape(str(details.get("year", "-")))
+ pages = escape(str(page_status))
director = escape(str(details.get("director", "-")))
genres = " / ".join(escape(str(genre)) for genre in details.get("genres",
[]))
+ runtime = escape(str(details.get("runtime", "147 min")))
+ language = escape(str(details.get("language", "Mandarin")))
+ region = escape(str(details.get("region", "China")))
+ movie_id = escape(str(details.get("movieId", "movie-1")))
summary = escape(str(details.get("summary", "")))
- version = escape(str(reviews.get("version", "unavailable")))
+ version_raw = str(reviews.get("version", "unavailable"))
+ version = escape(version_raw)
+ version_class = f"version-{version_raw}" if version_raw in {"v1", "v2",
"v3"} else "version-default"
+ detail_rows = "".join(
+ f"<div
class='metric'><span>{label}</span><strong>{value}</strong></div>"
+ for label, value in [
+ ("Release", year),
+ ("Pages", pages),
+ ("Director", director),
+ ("Genre", genres),
+ ("Runtime", runtime),
+ ("Language", language),
+ ("Region", region),
+ ("Movie ID", movie_id),
+ ]
+ )
review_items = "".join(
- f"<article
class='review-card'><span></span><p>{escape(str(item))}</p></article>"
- for item in reviews.get("items", [])
+ f"""
+ <article class="review-item">
+ <p>{escape(str(item))}</p>
+ <span>Viewer {index}</span>
+ </article>
+ """
+ for index, item in enumerate(reviews.get("items", []), start=1)
)
+ if not review_items:
+ review_items = "<p class='review-empty'>No audience notes
available.</p>"
rating = reviews.get("rating")
rating_html = "<p class='rating-empty'>No rating in this version</p>"
- if rating:
- color = escape(str(reviews.get("starColor", "#222")))
- stars = "★" * int(rating.get("stars", 0))
+ if isinstance(rating, dict):
+ score = rating.get("score", "9.0")
+ try:
+ score = f"{float(score):.1f}"
+ except (TypeError, ValueError):
+ score = escape(str(score))
rating_html = f"""
- <p class='stars' style='color: {color}'>{stars}</p>
- <p class='rating-source'>ratings service · movie-1</p>
+ <p class='score-number'>{score}</p>
+ <p class='rating-source'>ratings service · {movie_id}</p>
+ """
+ if user:
+ auth_html = f"""
+ <div class="identity">
+ <span>{escape(str(user))}</span>
+ <a href="/logout">Sign out</a>
+ </div>
+ """
+ else:
+ auth_html = """
+ <form class="signin" method="post" action="/login">
+ <input type="text" name="username" placeholder="Username"
autocomplete="username" required>
+ <input type="password" name="passwd" placeholder="Password"
autocomplete="current-password" required>
+ <button type="submit">Sign in</button>
+ </form>
"""
- return f"""<!doctype html>
+ html = f"""<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
@@ -74,14 +159,10 @@ def index():
<title>{title}</title>
<style>
:root {{
- --ink: #171313;
- --paper: #f7efe1;
- --muted: #766957;
- --line: rgba(23, 19, 19, .14);
- --wine: #8f2434;
- --teal: #126e72;
- --gold: #d6a84f;
- --night: #201b1a;
+ --ink: #333;
+ --muted: #666;
+ --line: #ddd;
+ --link: #337ab7;
}}
* {{ box-sizing: border-box; }}
@@ -90,281 +171,345 @@ def index():
min-height: 100vh;
margin: 0;
color: var(--ink);
- font-family: "Avenir Next", "Gill Sans", "Trebuchet MS", sans-serif;
- background:
- linear-gradient(90deg, rgba(32, 27, 26, .05) 1px, transparent 1px),
- linear-gradient(rgba(32, 27, 26, .05) 1px, transparent 1px),
- var(--paper);
- background-size: 36px 36px;
+ background: #fff;
+ font-family: Arial, Helvetica, sans-serif;
}}
- body::before {{
- position: fixed;
- inset: 0;
- z-index: -1;
- pointer-events: none;
- content: "";
- background:
- radial-gradient(circle at 18% 14%, rgba(214, 168, 79, .22),
transparent 28%),
- linear-gradient(135deg, rgba(143, 36, 52, .13), transparent 34%,
rgba(18, 110, 114, .13));
+ body.version-v1 {{
+ --version-bg: #337ab7;
+ --version-soft: #f5f9fc;
}}
- main {{
- width: min(1120px, calc(100% - 32px));
- margin: 0 auto;
- padding: 44px 0;
+ body.version-v2 {{
+ --version-bg: #8a6d3b;
+ --version-soft: #fcf8e3;
}}
- .shell {{
- display: grid;
- grid-template-columns: minmax(280px, 380px) minmax(0, 1fr);
- gap: 34px;
- align-items: stretch;
- }}
-
- .poster {{
- position: relative;
- min-height: 620px;
- overflow: hidden;
- color: #fff8ea;
- background:
- linear-gradient(150deg, rgba(143, 36, 52, .92), rgba(18, 110, 114,
.80) 55%, #151010),
- var(--night);
- border: 1px solid rgba(255, 255, 255, .24);
- border-radius: 8px;
- box-shadow: 0 28px 70px rgba(30, 20, 16, .32);
- isolation: isolate;
- }}
-
- .poster::before {{
- position: absolute;
- inset: 22px;
- content: "";
- border: 1px solid rgba(255, 248, 234, .32);
- border-radius: 4px;
+ body.version-v3 {{
+ --version-bg: #a94442;
+ --version-soft: #f9f2f4;
}}
- .poster::after {{
- position: absolute;
- right: -44px;
- bottom: -32px;
- z-index: -1;
- content: "影";
- font-family: Georgia, "Times New Roman", serif;
- font-size: 240px;
- line-height: 1;
- color: rgba(255, 248, 234, .10);
+ body.version-default {{
+ --version-bg: #555d66;
+ --version-soft: rgba(85, 93, 102, .10);
}}
- .poster-inner {{
- position: relative;
+ .topbar {{
+ width: 100%;
+ border-bottom: 1px solid var(--line);
+ background: #f8f8f8;
+ }}
+
+ .topbar-inner {{
display: flex;
- min-height: 620px;
- flex-direction: column;
+ min-height: 58px;
+ width: min(1120px, calc(100% - 48px));
+ margin: 0 auto;
+ align-items: center;
justify-content: space-between;
- padding: 36px;
+ gap: 18px;
}}
- .label {{
- width: max-content;
- padding: 6px 10px;
- border: 1px solid rgba(255, 248, 234, .36);
- color: #fff8ea;
- font-size: 12px;
- font-weight: 800;
- text-transform: uppercase;
+ .brand {{
+ color: var(--ink);
+ font-size: 18px;
+ font-weight: 500;
+ text-decoration: none;
}}
- h1 {{
- margin: 0;
- font-family: Georgia, "Times New Roman", serif;
- font-size: 58px;
- line-height: .98;
- font-weight: 700;
+ .signin,
+ .identity {{
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 8px;
}}
- .meta {{
- margin: 18px 0 0;
- color: rgba(255, 248, 234, .76);
- font-size: 15px;
- line-height: 1.6;
+ .identity {{
+ margin-left: auto;
}}
- .content {{
- display: grid;
- gap: 18px;
+ .signin input {{
+ width: 128px;
+ height: 34px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ padding: 0 10px;
+ color: var(--ink);
+ background: #fff;
+ font: inherit;
+ font-size: 13px;
+ outline: none;
+ }}
+
+ .signin input:focus {{ border-color: var(--version-bg); }}
+
+ .signin button,
+ .identity a {{
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ height: 34px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ padding: 0 12px;
+ color: #333;
+ background: #fff;
+ font: inherit;
+ font-size: 13px;
+ line-height: 1;
+ text-decoration: none;
+ cursor: pointer;
}}
- .summary,
- .reviews,
- .rating {{
- border: 1px solid var(--line);
- border-radius: 8px;
- background: rgba(255, 252, 244, .72);
- box-shadow: 0 20px 42px rgba(70, 48, 34, .10);
- backdrop-filter: blur(8px);
+ .identity span {{
+ color: var(--muted);
+ font-size: 13px;
}}
- .summary {{
- padding: 28px;
+ main {{
+ width: min(1120px, calc(100% - 48px));
+ margin: 0 auto;
+ padding: 32px 0 42px;
}}
- .section-kicker {{
- margin: 0 0 10px;
- color: var(--teal);
+ .movie-hero {{
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 220px;
+ gap: 32px;
+ align-items: end;
+ padding-bottom: 22px;
+ border-bottom: 1px solid var(--line);
+ }}
+
+ .kicker {{
+ margin: 0 0 8px;
+ color: var(--muted);
font-size: 12px;
- font-weight: 900;
+ font-weight: 700;
text-transform: uppercase;
}}
- .summary p {{
- max-width: 68ch;
+ h1 {{
margin: 0;
- color: #3f342b;
- font-size: 18px;
- line-height: 1.75;
+ color: var(--link);
+ font-size: 30px;
+ font-weight: 500;
+ line-height: 1.18;
}}
- .reviews {{
- padding: 24px;
+ .summary {{
+ max-width: 760px;
+ margin: 12px 0 0;
+ color: var(--ink);
+ font-size: 16px;
+ line-height: 1.5;
}}
- .reviews-head {{
- display: flex;
- gap: 16px;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 18px;
- border-bottom: 1px solid var(--line);
- padding-bottom: 16px;
+ .score-panel {{
+ padding: 0;
+ border: 0;
+ background: transparent;
+ text-align: right;
}}
- .reviews-head h2 {{
+ .version {{
+ display: inline-block;
+ margin-bottom: 10px;
+ padding: 4px 7px;
+ color: #fff;
+ background: var(--version-bg);
+ font-size: 11px;
+ font-weight: 700;
+ text-transform: uppercase;
+ }}
+
+ .score-number {{
margin: 0;
- font-family: Georgia, "Times New Roman", serif;
- font-size: 32px;
+ color: var(--version-bg);
+ font-size: 36px;
+ font-weight: 700;
line-height: 1;
+ white-space: nowrap;
}}
- .version {{
- flex: 0 0 auto;
- padding: 8px 12px;
- border-radius: 4px;
- background: var(--wine);
- color: #fff8ea;
- font-size: 13px;
- font-weight: 900;
+ .rating-source,
+ .rating-empty {{
+ margin: 6px 0 0;
+ color: var(--muted);
+ font-size: 12px;
}}
- .review-list {{
+ .meta-strip {{
display: grid;
- gap: 12px;
+ grid-template-columns: repeat(8, minmax(0, 1fr));
+ gap: 0;
+ padding: 14px 0;
+ border-bottom: 1px solid var(--line);
}}
- .review-card {{
- display: grid;
- grid-template-columns: 8px minmax(0, 1fr);
- gap: 14px;
- align-items: start;
- min-height: 74px;
- margin: 0;
- padding: 16px;
- border: 1px solid rgba(23, 19, 19, .10);
- border-radius: 6px;
- background: #fffaf0;
+ .metric {{
+ min-width: 0;
+ padding: 0 12px;
+ border-left: 1px solid var(--line);
+ }}
+
+ .metric:first-child {{ border-left: 0; padding-left: 0; }}
+
+ .metric span {{
+ display: block;
+ margin-bottom: 5px;
+ color: var(--muted);
+ font-size: 12px;
+ font-weight: 700;
+ text-transform: uppercase;
+ }}
+
+ .metric strong {{
+ display: block;
+ overflow-wrap: anywhere;
+ color: var(--ink);
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 1.35;
}}
- .review-card span {{
- width: 8px;
- height: 100%;
- min-height: 42px;
- border-radius: 999px;
- background: linear-gradient(var(--gold), var(--teal));
+ .review-section {{
+ display: grid;
+ grid-template-columns: 210px minmax(0, 1fr);
+ gap: 32px;
+ padding-top: 28px;
}}
- .review-card p {{
+ .section-head h2 {{
margin: 0;
- color: #302820;
- font-size: 17px;
- line-height: 1.65;
+ color: var(--link);
+ font-size: 22px;
+ font-weight: 500;
+ line-height: 1.2;
}}
- .rating {{
+ .review-list {{
display: grid;
- grid-template-columns: 1fr auto;
- gap: 20px;
- align-items: center;
- padding: 22px 24px;
+ gap: 18px;
}}
- .rating h2 {{
- margin: 0;
- font-family: Georgia, "Times New Roman", serif;
- font-size: 28px;
+ .review-item {{
+ padding-bottom: 18px;
+ border-bottom: 1px solid var(--line);
}}
- .stars {{
- margin: 0;
- font-size: 34px;
- letter-spacing: 0;
- text-shadow: 0 2px 0 rgba(0, 0, 0, .08);
- white-space: nowrap;
+ .review-item p {{
+ margin: 0 0 8px;
+ color: var(--ink);
+ font-size: 16px;
+ line-height: 1.5;
}}
- .rating-source,
- .rating-empty {{
- margin: 6px 0 0;
+ .review-item span,
+ .review-empty {{
color: var(--muted);
font-size: 13px;
}}
- @media (max-width: 820px) {{
- main {{ padding: 20px 0; }}
- .shell {{ grid-template-columns: 1fr; }}
- .poster,
- .poster-inner {{ min-height: 420px; }}
- h1 {{ font-size: 42px; }}
- .rating {{ grid-template-columns: 1fr; }}
- .reviews-head {{ align-items: flex-start; flex-direction: column; }}
+ @media (max-width: 900px) {{
+ .topbar-inner {{
+ width: min(100% - 32px, 1120px);
+ align-items: center;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ padding: 12px 0;
+ }}
+
+ .signin {{
+ width: auto;
+ margin-left: auto;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ }}
+
+ main {{
+ width: min(100% - 32px, 1120px);
+ padding-top: 28px;
+ }}
+
+ .movie-hero,
+ .review-section {{
+ grid-template-columns: 1fr;
+ gap: 20px;
+ }}
+
+ h1 {{ font-size: 26px; }}
+
+ .summary {{ font-size: 15px; }}
+
+ .score-panel {{
+ max-width: 320px;
+ text-align: left;
+ }}
+
+ .meta-strip {{
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ row-gap: 14px;
+ }}
+
+ .metric:nth-child(odd) {{
+ border-left: 0;
+ padding-left: 0;
+ }}
+ }}
+
+ @media (max-width: 560px) {{
+ .topbar-inner {{
+ align-items: flex-start;
+ flex-direction: column;
+ justify-content: center;
+ }}
+
+ .signin {{
+ width: 100%;
+ margin-left: 0;
+ justify-content: flex-start;
+ }}
+
+ .identity {{
+ margin-left: 0;
+ }}
}}
</style>
</head>
-<body>
- <main>
- <div class="shell">
- <section class="poster">
- <div class="poster-inner">
- <span class="label">Movie Review</span>
- <div>
- <h1>{title}</h1>
- <p class="meta">{year} · {director}<br>{genres}</p>
- </div>
- </div>
- </section>
- <section class="content">
- <article class="summary">
- <p class="section-kicker">Synopsis</p>
- <p>{summary}</p>
- </article>
- <article class="reviews">
- <div class="reviews-head">
- <h2>Audience Notes</h2>
- <span class="version">reviews {version}</span>
- </div>
- <div class="review-list">{review_items}</div>
- </article>
- <article class="rating">
- <div>
- <p class="section-kicker">Score</p>
- <h2>Viewer Rating</h2>
- </div>
- <div>{rating_html}</div>
- </article>
- </section>
+<body class="{version_class}">
+ <header class="topbar">
+ <div class="topbar-inner">
+ <a class="brand" href="/">MovieReview Sample</a>
+ {auth_html}
</div>
+ </header>
+ <main>
+ <section class="movie-hero">
+ <div>
+ <p class="kicker">Movie Review</p>
+ <h1>{title}</h1>
+ <p class="summary">{summary}</p>
+ </div>
+ <div class="score-panel">
+ <span class="version">reviews {version}</span>
+ {rating_html}
+ </div>
+ </section>
+ <section class="meta-strip" aria-label="Movie
details">{detail_rows}</section>
+ <section class="review-section">
+ <div class="section-head">
+ <h2>Audience Reviews</h2>
+ </div>
+ <div class="review-list">{review_items}</div>
+ </section>
</main>
</body>
</html>"""
+ return html, page_status
if __name__ == "__main__":
diff --git a/samples/moviereview/src/ratings/main.go
b/samples/moviereview/src/ratings/main.go
index c1028ffe..c3777802 100644
--- a/samples/moviereview/src/ratings/main.go
+++ b/samples/moviereview/src/ratings/main.go
@@ -23,7 +23,7 @@ import (
type rating struct {
MovieID string `json:"movieId"`
- Stars int `json:"stars"`
+ Score string `json:"score"`
}
func main() {
@@ -39,7 +39,7 @@ func main() {
if movieID == "" {
movieID = "movie-1"
}
- writeJSON(w, rating{MovieID: movieID, Stars: 5})
+ writeJSON(w, rating{MovieID: movieID, Score: "9.0"})
})
log.Fatal(http.ListenAndServe(":9080", nil))
}
diff --git a/samples/moviereview/src/reviews-v2/app.py
b/samples/moviereview/src/reviews-v2/app.py
index 7122ca77..ede89f5f 100644
--- a/samples/moviereview/src/reviews-v2/app.py
+++ b/samples/moviereview/src/reviews-v2/app.py
@@ -49,7 +49,6 @@ def reviews():
"这个版本会调用 ratings 服务。"
],
"rating": rating(),
- "starColor": "#111827",
}
)
diff --git a/samples/moviereview/src/reviews-v3/server.js
b/samples/moviereview/src/reviews-v3/server.js
index 84e5d121..30ac82c4 100644
--- a/samples/moviereview/src/reviews-v3/server.js
+++ b/samples/moviereview/src/reviews-v3/server.js
@@ -74,7 +74,6 @@ const server = http.createServer(async (req, res) => {
version: "v3",
items: ["评论样式更醒目。", "这个版本也调用 ratings 服务。"],
rating,
- starColor: "#dc2626",
}),
);
} catch (err) {