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 &lt;token&gt;</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>&lt;@theme&gt;
173            &lt;@token name="--jc-pill-red-bg" value="#fdeceb"/&gt;
174            &lt;@token name="--jc-tab-bar-bg" alias="var(--jc-card-bg)"/&gt;
175            &lt;@token name="--jc-link" value="#d9a65c" scheme="dark"/&gt;
176            &lt;/@theme&gt;</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}