This is an automated email from the ASF dual-hosted git repository. Xuanwo pushed a commit to branch xuanwo/website-redesign in repository https://gitbox.apache.org/repos/asf/opendal.git
commit f4337bdc2b0f38a4f0f5ce0e81e3d9d95afed2d7 Author: Xuanwo <[email protected]> AuthorDate: Sat May 30 01:56:52 2026 +0800 feat(website): make layers an interactive explorer and polish landing sections --- website/src/components/landing/data.js | 118 +++++++++++++--------- website/src/components/landing/sections.jsx | 118 +++++++++------------- website/src/components/landing/styles.module.css | 123 ++++++++--------------- website/src/pages/index.jsx | 2 - 4 files changed, 157 insertions(+), 204 deletions(-) diff --git a/website/src/components/landing/data.js b/website/src/components/landing/data.js index 83935b611..3a7b786fd 100644 --- a/website/src/components/landing/data.js +++ b/website/src/components/landing/data.js @@ -73,9 +73,8 @@ data = op.read("hello.txt")`, ) // Configure a backend once, then use one Operator. -op, _ := opendal.NewOperator(s3.Scheme, opendal.OperatorOptions{ - "bucket": "data", -}) +opts := opendal.OperatorOptions{"bucket": "data"} +op, _ := opendal.NewOperator(s3.Scheme, opts) op.Write("hello.txt", []byte("Hello, World!")) data, _ := op.Read("hello.txt")`, @@ -110,15 +109,6 @@ const data = await op.read("hello.txt");`, }, ]; -// Layered example used in the Layers section. -export const layeredCode = `use opendal::layers::{LoggingLayer, RetryLayer}; -use opendal::Operator; - -// Cross-cutting behavior composes onto any operator, in order. -let op = op - .layer(RetryLayer::new()) - .layer(LoggingLayer::default());`; - export const valueProps = [ { index: "01", @@ -147,6 +137,7 @@ export const valueProps = [ // read / write / manage APIs (verified against types/operator + types/options). const RS = "https://docs.rs/opendal/latest/opendal"; const opDoc = (method) => `${RS}/struct.Operator.html#method.${method}`; +const layerDoc = (name) => `${RS}/layers/struct.${name}.html`; export const capabilityThemes = [ { @@ -215,7 +206,8 @@ op.copy("draft.md", "final.md").await?; // Recursively delete a subtree. op.delete_with("tmp/").recursive(true).await?; // Presign a temporary, shareable URL. -let url = op.presign_read("report.csv", Duration::from_secs(3600)).await?;`, +let ttl = Duration::from_secs(3600); +let url = op.presign_read("report.csv", ttl).await?;`, }, ]; @@ -359,65 +351,93 @@ export const bindings = [ export const layers = [ { name: "RetryLayer", - icon: "/img/layers/retry.svg", desc: "Recover from transient failures automatically.", + doc: layerDoc("RetryLayer"), + code: `use opendal::layers::RetryLayer; + +// Exponential backoff with jitter; interrupted +// reads and writes resume where they left off. +let op = op.layer( + RetryLayer::new().with_max_times(5).with_jitter(), +);`, }, { name: "TimeoutLayer", - icon: "/img/layers/timeout.svg", desc: "Bound slow or hanging operations.", + doc: layerDoc("TimeoutLayer"), + code: `use opendal::layers::TimeoutLayer; +use std::time::Duration; + +// Abort operations that stall past a deadline. +let op = op.layer( + TimeoutLayer::new() + .with_timeout(Duration::from_secs(10)), +);`, }, { name: "LoggingLayer", - icon: "/img/layers/logging.svg", desc: "Emit structured operation logs.", + doc: layerDoc("LoggingLayer"), + code: `use opendal::layers::LoggingLayer; + +// Structured logs for every operation, via the +// standard log crate facade. +let op = op.layer(LoggingLayer::default());`, }, { name: "TracingLayer", - icon: "/img/layers/tracing.svg", desc: "Trace requests across systems.", + doc: layerDoc("TracingLayer"), + code: `use opendal::layers::TracingLayer; + +// One span per operation, into whatever +// tracing subscriber your app installs. +let op = op.layer(TracingLayer::new());`, }, { name: "MetricsLayer", - icon: "/img/layers/metrics.svg", desc: "Export operation metrics.", + doc: layerDoc("MetricsLayer"), + code: `use opendal::layers::MetricsLayer; + +// Latency and throughput via the metrics crate +// facade — plug in any exporter you like. +let op = op.layer(MetricsLayer::default());`, }, { name: "PrometheusLayer", - icon: "/img/layers/prometheus.svg", desc: "Expose Prometheus metrics.", - }, - { - name: "Traffic Control", - icon: "/img/layers/traffic-control.svg", - desc: "Throttle and cap concurrency.", - }, - { - name: "FoyerLayer", - icon: "/img/layers/cache.svg", - desc: "Add hybrid cache behavior.", - }, -]; + doc: layerDoc("PrometheusLayer"), + code: `use opendal::layers::PrometheusLayer; -export const principles = [ - { - title: "Open Community", - body: "An ASF project governed by its PMC. Community over code, the Apache Way.", - }, - { - title: "Solid Foundation", - body: "A dependable base you can trust for real-world storage operations.", - }, - { - title: "Fast Access", - body: "Zero-overhead access — as fast as, or faster than, each native SDK.", - }, - { - title: "Object Storage First", - body: "Designed and optimized for modern object storage from the ground up.", +// Register operation metrics into a Prometheus +// registry you already expose. +let registry = prometheus::default_registry(); +let op = op.layer( + PrometheusLayer::builder().register(registry)?, +);`, + }, + { + name: "ConcurrentLimitLayer", + desc: "Cap in-flight concurrency.", + doc: layerDoc("ConcurrentLimitLayer"), + code: `use opendal::layers::ConcurrentLimitLayer; + +// Cap how many operations hit the backend at once — +// back-pressure for the whole Operator. +let op = op.layer(ConcurrentLimitLayer::new(1024));`, }, { - title: "Extensible Architecture", - body: "Independent services, layers and bindings you can stack and extend.", + name: "ThrottleLayer", + desc: "Throttle I/O bandwidth.", + doc: layerDoc("ThrottleLayer"), + code: `use opendal::layers::ThrottleLayer; + +// Token-bucket bandwidth limit: ~10 MiB/s steady, +// with headroom to burst for short spikes. +let op = op.layer(ThrottleLayer::new( + 10 * 1024 * 1024, + 32 * 1024 * 1024, +));`, }, ]; diff --git a/website/src/components/landing/sections.jsx b/website/src/components/landing/sections.jsx index cfb1d7d16..f3f65506d 100644 --- a/website/src/components/landing/sections.jsx +++ b/website/src/components/landing/sections.jsx @@ -36,8 +36,6 @@ import { serviceGroups, bindings, layers, - layeredCode, - principles, } from "./data"; export function Hero() { @@ -313,79 +311,60 @@ export function Bindings() { } export function Layers() { - const { withBaseUrl } = useBaseUrlUtils(); + const [active, setActive] = useState(layers[0]); return ( <section className={styles.section}> - <div className="odl-container"> - <div className={styles.layersInner}> - <div> - <span className="odl-eyebrow">Layers</span> - <h2 className={styles.sectionTitle}> - Production behavior, composed — not coded. - </h2> - <p className={styles.sectionLede}> - Stack cross-cutting concerns as reusable layers. The order is - explicit and the core stays zero-cost. - </p> - <div style={{ marginTop: "var(--odl-space-6)" }}> - <CodeTabs - samples={[ - { - id: "layered", - label: "Rust", - language: "rust", - code: layeredCode, - }, - ]} - title="compose layers onto any operator" - /> - </div> - </div> - <div className={`${styles.layerGrid} ${styles.reveal}`}> - {layers.map((l) => ( - <div className={styles.layerItem} key={l.name}> - <img - className={styles.layerIcon} - src={withBaseUrl(l.icon)} - alt="" - width="26" - height="26" - loading="lazy" - /> - <div> - <p className={styles.layerName}>{l.name}</p> - <p className={styles.layerDesc}>{l.desc}</p> - </div> - </div> - ))} - </div> - </div> - </div> - </section> - ); -} - -export function Community() { - return ( - <section className={`${styles.section} ${styles.sectionSubtle}`}> <div className="odl-container"> <div className={styles.sectionHead}> - <span className="odl-eyebrow">Open the Apache Way</span> + <span className="odl-eyebrow">Layers</span> <h2 className={styles.sectionTitle}> - Built by a community, for the commons. + Production behavior, composed — not coded. </h2> <p className={styles.sectionLede}> - Apache OpenDAL™ graduated to a top-level project in 2024. Five - principles guide every decision we make. + Stack cross-cutting concerns as reusable layers. The order is + explicit and the core stays zero-cost. </p> </div> - <div className={styles.principleGrid}> - {principles.map((p) => ( - <div className={styles.principle} key={p.title}> - <h3 className={styles.principleTitle}>{p.title}</h3> - <p className={styles.principleBody}>{p.body}</p> + <div className={styles.layerExplorer}> + <div className={styles.layerGrid}> + {layers.map((l) => { + const selected = active.name === l.name; + return ( + <Link + key={l.name} + className={`${styles.layerItem} ${ + selected ? styles.layerItemActive : "" + }`} + to={l.doc} + target="_blank" + rel="noreferrer" + aria-current={selected ? "true" : undefined} + onMouseEnter={() => setActive(l)} + onFocus={() => setActive(l)} + > + <span className={styles.layerName}>{l.name}</span> + <span className={styles.layerDesc}>{l.desc}</span> + </Link> + ); + })} + </div> + <div className={styles.capabilityPreview}> + <div className={styles.codeWindow}> + <div className={styles.windowBar}> + <div className={styles.windowDots} aria-hidden="true"> + <span /> + <span /> + <span /> + </div> + <span className={styles.windowTitle}>{active.name}</span> + </div> + <div className={`${styles.codeBody} ${styles.layerCodeBody}`}> + <div className={styles.capabilityCodeFade} key={active.name}> + <CodeBlock language="rust">{active.code}</CodeBlock> + </div> + </div> </div> - ))} + </div> </div> </div> </section> @@ -394,10 +373,9 @@ export function Community() { export function FinalCta() { return ( - <section className={styles.section}> + <section className={`${styles.section} ${styles.sectionSubtle}`}> <div className="odl-container"> <div className={styles.finalCta}> - <div className={styles.finalGrid} aria-hidden="true" /> <div className={`${styles.finalCtaInner} ${styles.finalCenter}`}> <span className={`odl-eyebrow ${styles.finalEyebrow}`}> Start building @@ -411,19 +389,19 @@ export function FinalCta() { </p> <div className={styles.finalCtaActions}> <Link - className={`${styles.btn} ${styles.btnOnDark}`} + className={`${styles.btn} ${styles.btnPrimary}`} to={DOCS_URL} > Get started <span className={styles.btnArrow}>→</span> </Link> <Link - className={`${styles.btn} ${styles.btnOnDarkGhost}`} + className={`${styles.btn} ${styles.btnSecondary}`} to={REPO_URL} > Star on GitHub </Link> <Link - className={`${styles.btn} ${styles.btnOnDarkGhost}`} + className={`${styles.btn} ${styles.btnSecondary}`} to={DISCORD_URL} > Join Discord diff --git a/website/src/components/landing/styles.module.css b/website/src/components/landing/styles.module.css index b76032d14..8b1559bbe 100644 --- a/website/src/components/landing/styles.module.css +++ b/website/src/components/landing/styles.module.css @@ -270,6 +270,14 @@ margin: 0; background: var(--odl-code-bg) !important; font-size: 0.8125rem; + /* Wrap long lines so code windows never scroll horizontally. */ + white-space: pre-wrap; + overflow-wrap: anywhere; +} +.codeBody :global(pre code), +.codeBody :global(.token-line) { + white-space: inherit; + overflow-wrap: inherit; } /* ===== Used-by wall ===================================================== */ @@ -459,7 +467,7 @@ top: 5rem; } .capabilityCodeBody { - height: 18rem; + height: 19rem; overflow: auto; background: var(--odl-code-bg); } @@ -580,11 +588,11 @@ color: var(--odl-fg-muted); } -/* ===== Layers =========================================================== */ -.layersInner { +/* ===== Layers (tile grid left, live code preview right) ================= */ +.layerExplorer { display: grid; - grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr); - gap: clamp(2rem, 1rem + 4vw, 4rem); + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: clamp(2rem, 1rem + 3vw, 3.5rem); align-items: start; } .layerGrid { @@ -597,83 +605,51 @@ overflow: hidden; } .layerItem { - background: var(--odl-surface); - padding: var(--odl-space-4); display: flex; - gap: var(--odl-space-3); - align-items: flex-start; + flex-direction: column; + gap: 4px; + padding: var(--odl-space-4); + background: var(--odl-surface); + text-decoration: none; + transition: background-color var(--odl-dur) var(--odl-ease); } -.layerIcon { - width: 26px; - height: 26px; - flex: none; - object-fit: contain; - margin-top: 2px; +.layerItem:hover { + background: var(--odl-surface-2); + text-decoration: none; +} +.layerItemActive { + background: var(--odl-surface-2); } .layerName { font-family: var(--odl-font-mono); font-size: var(--odl-text-sm); font-weight: 600; color: var(--odl-fg-strong); - margin: 0; + transition: color var(--odl-dur) var(--odl-ease); +} +.layerItemActive .layerName { + color: var(--odl-accent); } .layerDesc { font-size: var(--odl-text-xs); line-height: var(--odl-leading-snug); color: var(--odl-fg-muted); - margin: 2px 0 0; } - -/* ===== Community / principles =========================================== */ -.principleGrid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); - gap: var(--odl-space-5); - margin-top: var(--odl-space-7); -} -.principle { - border-top: 2px solid var(--odl-bar); - padding-top: var(--odl-space-4); -} -.principleTitle { - font-size: var(--odl-text-md); - font-weight: 650; - color: var(--odl-fg-strong); - margin: 0 0 var(--odl-space-2); - letter-spacing: var(--odl-tracking-tight); -} -.principleBody { - font-size: var(--odl-text-sm); - line-height: var(--odl-leading-snug); - color: var(--odl-fg-muted); - margin: 0; +.layerCodeBody { + height: 13rem; + overflow: auto; + background: var(--odl-code-bg); } /* ===== Final CTA ======================================================== */ .finalCta { - position: relative; - overflow: hidden; - background: var(--odl-ink-950); - color: #fff; + background: var(--odl-surface); + border: 1px solid var(--odl-border); border-radius: var(--odl-radius-lg); + box-shadow: var(--odl-shadow-sm); padding: clamp(2.5rem, 1.5rem + 5vw, 5rem); text-align: center; } -[data-theme="dark"] .finalCta { - border: 1px solid var(--odl-border); -} -.finalGrid { - position: absolute; - inset: 0; - background-image: linear-gradient( - rgba(255, 255, 255, 0.05) 1px, - transparent 1px - ), - linear-gradient(90deg, rgba(255, 255, 255, 0.05) 1px, transparent 1px); - background-size: 44px 44px; - -webkit-mask-image: radial-gradient(80% 80% at 50% 0%, #000, transparent 75%); - mask-image: radial-gradient(80% 80% at 50% 0%, #000, transparent 75%); -} .finalCtaInner { position: relative; } @@ -682,11 +658,11 @@ font-weight: 700; letter-spacing: var(--odl-tracking-tight); margin: var(--odl-space-4) 0 0; - color: #fff; + color: var(--odl-fg-strong); } .finalCtaLede { font-size: var(--odl-text-md); - color: rgba(255, 255, 255, 0.7); + color: var(--odl-fg-muted); margin: var(--odl-space-4) auto 0; max-width: 34rem; } @@ -699,25 +675,6 @@ } .finalEyebrow { justify-content: center; - color: rgba(255, 255, 255, 0.75); -} -.btnOnDark { - background: #fff; - color: var(--odl-ink-950); -} -.btnOnDark:hover { - background: rgba(255, 255, 255, 0.88); - color: var(--odl-ink-950); -} -.btnOnDarkGhost { - background: transparent; - color: #fff; - border-color: rgba(255, 255, 255, 0.28); -} -.btnOnDarkGhost:hover { - color: #fff; - border-color: rgba(255, 255, 255, 0.6); - background: rgba(255, 255, 255, 0.06); } .finalCenter { display: flex; @@ -751,8 +708,8 @@ /* ===== Responsive ======================================================= */ @media (max-width: 996px) { .heroInner, - .layersInner, - .capabilityExplorer { + .capabilityExplorer, + .layerExplorer { grid-template-columns: 1fr; } .capabilityPreview { diff --git a/website/src/pages/index.jsx b/website/src/pages/index.jsx index 9d8259343..b6b6c7b88 100644 --- a/website/src/pages/index.jsx +++ b/website/src/pages/index.jsx @@ -27,7 +27,6 @@ import { Services, Bindings, Layers, - Community, FinalCta, } from "../components/landing/sections"; @@ -45,7 +44,6 @@ export default function Home() { <Services /> <Bindings /> <Layers /> - <Community /> <FinalCta /> </main> </Layout>
