001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.petstore.console.pages; 018 019import static org.apache.juneau.commons.utils.StringUtils.*; 020 021import java.util.*; 022import java.util.stream.*; 023 024import org.apache.juneau.marshall.collections.*; 025import org.apache.juneau.petstore.console.dev.*; 026import org.apache.juneau.petstore.console.dev.flavors.*; 027import org.apache.juneau.console.*; 028import org.apache.juneau.console.TopicDecl.*; 029import org.apache.juneau.console.freemarker.*; 030import org.apache.juneau.console.freemarker.DataSourceDef.*; 031import org.apache.juneau.console.views.*; 032import org.apache.juneau.console.views.TableSpec.*; 033 034/** 035 * Builder twins of the Developer pages: Custom cards ({@code cards.ftlh}), Rendering flavors ({@code flavors.ftlh}, 036 * {@code flavors-react.ftlh}), Secure ({@code secure.ftlh}), Themes ({@code themes.ftlh}), Charts (builder only), 037 * Data sources (builder only) and Dashboard (builder only). 038 * 039 * <h5 class='section'>Example:</h5> 040 * <p class='bjava'> 041 * <jk>return</jk> <jsm>twin</jsm>(<jv>req</jv>, DevPageBuilders.<jsm>secure</jsm>()); 042 * </p> 043 */ 044@SuppressWarnings({ 045 "java:S1192" // Page-definition ids and labels repeat as inline literals by design. 046}) 047public final class DevPageBuilders { 048 049 private DevPageBuilders() {} 050 051 /** 052 * Custom cards. 053 * 054 * <p> 055 * The gauge card's body is the same {@code {species, low, high}} the FTL page authors: {@link PageSpec} runs it 056 * through the registered {@link InventoryGaugeCardType}, so both pages carry the same validated fragment. 057 * 058 * @return The page spec. 059 */ 060 public static PageSpec cards() { 061 var gauge = CardSpec.of(InventoryGaugeCardType.TYPE, "gauge").title("Dog inventory") 062 .body("species", "DOG").body("low", 20).body("high", 120); 063 return PageSpec.create().tab("dev/cards").init("/console/dev/cards/petstore-cards.js") 064 .card(CardSpec.of("kpi", "available").title("Pets available") 065 .body("label", "Pets available").body("dataUrl", "/console/dev/cards/stats/available")) 066 .card(gauge) 067 .card(ChartDef.bar("bands").title("Stock by species").src("/console/dev/cards/inventory/all") 068 .x("species").series("available", "Available").series("pending", "Pending").series("sold", "Sold") 069 .stacked(true).horizontal().decorate("petstore.stockTotals", null).on("click", "app.band-picked").toCardSpec()) 070 .card(ChartDef.scatter("prices").title("Prices by pet").src("/console/dev/cards/prices") 071 .x("id", "number", "Pet").series("price", "Price") 072 .option("{dataZoom:[{type:'inside'},{type:'slider'}]}").publishZoom().toCardSpec()) 073 .html("toolbar", "Card API", """ 074 <div class="jc-prose"> 075 <p> 076 <button type="button" class="jc-btn jc-btn-outline" data-cards-action="refresh">Refresh the gauge</button> 077 <button type="button" class="jc-btn jc-btn-outline" data-cards-action="api">Read the gauge API</button> 078 <button type="button" class="jc-btn jc-btn-outline" data-cards-action="cat">Show cats</button> 079 </p> 080 <output id="cards-api-result" aria-live="polite"></output> 081 </div>""") 082 .html("how", "Writing a card type", """ 083 <div class="jc-prose"> 084 <p>A card type is a JS handler, and optionally a Java class that validates the body and shapes what the handler 085 receives. Push handlers onto <code>window.JuneauConsoleCards</code>; the queue works whether your script loads before 086 or after the console.</p> 087 <pre><code>(window.JuneauConsoleCards = window.JuneauConsoleCards || []).push(['kpi', { 088 render(card, el, ctx) { ... } 089 }]);</code></pre> 090 <p>The gauge's Java side is <code>InventoryGaugeCardType</code>, registered with 091 <code>ConsoleFreemarkerMixin.create().cardType(new InventoryGaugeCardType())</code>.</p> 092 </div>"""); 093 } 094 095 /** 096 * One Rendering flavors page. 097 * 098 * @param flavor The flavor. 099 * @return The page spec. 100 */ 101 public static PageSpec flavor(Flavor flavor) { 102 var p = PageSpec.create().tab("dev/flavors/" + flavor.id()).toolkit("views") 103 .html("caption", flavor.label(), "<div class=\"jc-prose\"><p>%s</p><p><a href=\"%s\">View source</a></p></div>" 104 .formatted(escapeHtml(flavor.caption()), escapeHtml(flavor.sourceUrl()))); 105 if (flavor == Flavor.REACT) 106 return p.html("flavor", "React app", 107 "<iframe src=\"/petstore-ui\" title=\"Petstore React app\" class=\"petstore-flavor-frame\" width=\"100%\" height=\"640\"></iframe>"); 108 return p.htmlSrc("flavor", "Pets", "/console/dev/flavors/" + flavor.id() + "/fragment"); 109 } 110 111 /** 112 * Secure. 113 * 114 * @return The page spec. 115 */ 116 public static PageSpec secure() { 117 return PageSpec.create().tab("dev/secure").init("/console/dev/secure/secure.js") 118 .html("about", "Bearer-token guard", """ 119 <div class="jc-prose"> 120 <p>This page is open. The API under <code>/console/dev/secure/api</code> is not: every call needs an 121 <code>Authorization: Bearer <token></code> header, checked by a fail-closed guard.</p> 122 <p>Demo tokens: <code>petstore-user</code> (alice) and <code>petstore-admin</code> (admin). Anything else is a 401.</p> 123 <pre><code>curl -H 'Authorization: Bearer petstore-user' http://localhost:10000/console/dev/secure/api/pets</code></pre> 124 </div>""") 125 .html("try", "Try it", """ 126 <div class="jc-prose"> 127 <p> 128 <button type="button" data-secure-call="none" data-secure-url="/console/dev/secure/api/whoami">Without a token</button> 129 <button type="button" data-secure-call="petstore-user" data-secure-url="/console/dev/secure/api/whoami">With the demo token</button> 130 <button type="button" data-secure-call="wrong-token" data-secure-url="/console/dev/secure/api/whoami">With a wrong token</button> 131 </p> 132 <pre><output id="secure-result" aria-live="polite"></output></pre> 133 </div>"""); 134 } 135 136 /** 137 * Themes. 138 * 139 * <p> 140 * The theme itself is drawn by the chrome ({@code base.ftlh}), so the spec carries the same 141 * {@code petstoreTheme} / {@code petstoreCustomTheme} attributes the FTL page does. 142 * 143 * @param theme The stock theme to draw in (the custom theme's seed when {@code custom}). 144 * @param custom Whether the custom tokens are applied. 145 * @param choices The theme names to offer. 146 * @return The page spec. 147 */ 148 public static PageSpec themes(String theme, boolean custom, List<String> choices) { 149 var links = choices.stream().map(x -> escapeHtml(x)).map(x -> "<li><a href=\"/console/dev/themes?theme=" + x + "\">" + x + "</a></li>") 150 .collect(Collectors.joining()); 151 return PageSpec.create().tab("dev/themes") 152 .attr("petstoreTheme", theme).attr("petstoreCustomTheme", custom) 153 .html("swatches", "This theme", """ 154 <div class="jc-prose"> 155 <p>The page is drawn in <strong>%s</strong>%s.</p> 156 <p> 157 <span class="tag stage released">Available</span> 158 <span class="tag stage pending">Pending</span> 159 <span class="tag stage failed">Sold</span> 160 </p> 161 </div>""".formatted(escapeHtml(theme), custom ? ", with custom tokens" : "")) 162 .html("picker", "Pick a theme", """ 163 <div class="jc-prose"> 164 <ul> 165 %s 166 </ul> 167 </div>""".formatted(links)) 168 .html("authoring", "Writing a custom theme", """ 169 <div class="jc-prose"> 170 <p>Seed from the page theme and override tokens. A <code>value</code> sets a token; an <code>alias</code> makes it 171 follow another token. Add <code>scheme="dark"</code> to set a token for the dark scheme only.</p> 172 <pre><code><@theme> 173 <@token name="--jc-pill-red-bg" value="#fdeceb"/> 174 <@token name="--jc-tab-bar-bg" alias="var(--jc-card-bg)"/> 175 <@token name="--jc-link" value="#d9a65c" scheme="dark"/> 176 </@theme></code></pre> 177 </div>"""); 178 } 179 180 /** 181 * Charts (builder only): a line chart with markers, a threshold and a click topic, and a table whose ribbon highlights a point. 182 * 183 * @return The page spec. 184 */ 185 public static PageSpec charts() { 186 var rows = "/console/dev/charts/daily"; 187 return PageSpec.create().tab("dev").toolkit("views") 188 .card(ChartDef.line("trend").title("Orders this week").src(rows).x("day") 189 .series("placed", "Placed").series("delivered", "Delivered") 190 .decorate(Markers.values(List.of(Map.of("at", "Wed", "label", "Restock")))) 191 .decorate(Thresholds.at(6).label("Target").tone("ok")) 192 .on("click", "app.day-picked") 193 .toCardSpec()) 194 .table(TableSpec.create("days").dataMode(DataMode.CLIENT).dataUrl(rows) 195 .columns(Column.create("day").label("Day"), Column.create("placed").label("Placed"), Column.create("delivered").label("Delivered")) 196 .ribbon(RibbonItem.publish("Highlight Monday", "cmd:trend", 197 Map.of("schemaVersion", 1, "op", "highlight", "options", Map.of("seriesId", "placed", "dataIndex", 0)))), 198 "Days"); 199 } 200 201 /** 202 * Data sources (builder only): an hourly CSV source chunked by UTC day and cached in IndexedDB, over a range the 203 * page script publishes, drawn by a line chart. 204 * 205 * @return The page spec. 206 */ 207 public static PageSpec data() { 208 var range = Topics.custom("petstore", "range"); 209 return PageSpec.create().tab("dev").toolkit("views").init("/console/dev/data/data.js") 210 .topic(TopicDecl.of(range).retain(true).publisher(Publisher.SCRIPT)) 211 .data(DataSourceDef.create("orders").url("/console/dev/data/orders").format(Format.CSV) 212 .columns("t:time", "placed:number") 213 .params(Subscription.to(range).as("params").map("start", "start").map("end", "end")) 214 .chunk(Chunking.byDay("start", "end")).cache(Cache.indexedDb())) 215 .card(ChartDef.line("ordersChart").title("Orders by hour").source(Topics.data("orders")).x("t") 216 .series("placed", "Placed").toCardSpec()); 217 } 218 219 /** 220 * Dashboard (builder only): a rail of every section kind over a 2-column grid of charts, bound to one CSV source. 221 * 222 * @param variant The source variant: {@code ok}, {@code slow}, {@code truncated}, {@code error} or {@code right} (the {@code ok} source with the rail on the right). 223 * @param version The persist version; bumping it discards saved state. 224 * @return The page spec. 225 */ 226 public static PageSpec dashboard(String variant, int version) { 227 var base = "/console/dev/dashboard"; 228 var share = ChartDef.donut("share").title("Share by reason").src(base + "/reasons").label("reason").value("orders"); 229 return PageSpec.create().tab("dev").toolkit("views").init(base + "/dashboard.js") 230 // An array param (reasons) repeats its key in the query, which the endpoint reads with getAll. 231 .data(DataSourceDef.create("orders").url(base + "/series/" + variant).format(Format.CSV) 232 .columns("day:time", "reason:string", "orders:number") 233 .params( 234 Subscription.to(Topics.filter("range")).as("params").map("start", "start").map("end", "end"), 235 Subscription.to(Topics.filter("q")).as("params").map("q", "search"), 236 Subscription.to(Topics.filter("reasons")).as("params").map("reasons", "columns.reason"), 237 Subscription.to(Topics.filter("late")).as("params").map("lateOnly", "options.lateOnly")) 238 .chunk(Chunking.none()).cache(Cache.none())) 239 .dashboard(DashboardSpec.create("overview").title("Order issues") 240 .rail(("right".equals(variant) ? Rail.right() : Rail.left()).sections( 241 RangeSection.quick("7d", "30d", "ytd").custom(true).defaultPreset("30d").publishes(Topics.filter("range")), 242 SearchSection.create().placeholder("Reason contains").publishes(Topics.filter("q")), 243 ChoiceSection.fromDim("orders", "reason").label("Reasons").publishes(Topics.filter("reasons")), 244 CardSection.of(share), 245 Section.custom("petstore.late", JsonMap.of("label", "Late only")).publishes(Topics.filter("late")))) 246 .grid(2) 247 .panel(ChartDef.line("daily").title("Orders per day").source(Topics.data("orders")).x("day", "time", "Day") 248 .series("orders", "Orders").publishZoom().patch("{dataZoom:[{type:'inside'},{type:'slider'}]}"), 2) 249 .panel(ChartDef.bar("by-reason").title("Orders by reason").src(base + "/reasons").x("reason").series("orders", "Orders")) 250 .panel(ChartDef.bar("by-weekday").title("Orders by weekday").src(base + "/weekdays").x("weekday").series("orders", "Orders")) 251 .status(Topics.data("orders")) 252 .persist(PersistSpec.page("petstore.dashboard").version(version) 253 .topics(Topics.filter("range"), Topics.filter("q"), Topics.filter("reasons"), Topics.zoom("daily")).shareLink())); 254 } 255}