diff options
author | Sebastian <sebasjm@gmail.com> | 2022-06-05 23:10:51 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2022-06-05 23:11:38 -0300 |
commit | abb47b60ad6aa82f68c88c10b0fa614785cd123c (patch) | |
tree | 79fa39379ece376dd106c3865ca4b2f8531dc30d /packages | |
parent | fee5de75624a1d8e42ac695876ab188ab2d92921 (diff) |
dev script without storybook
Diffstat (limited to 'packages')
-rwxr-xr-x | packages/anastasis-webui/clean_and_build.sh | 2 | ||||
-rwxr-xr-x | packages/anastasis-webui/dev.mjs | 83 | ||||
-rw-r--r-- | packages/anastasis-webui/package.json | 5 | ||||
-rw-r--r-- | packages/anastasis-webui/src/pages/home/index.storiesNo.tsx | 80 | ||||
-rw-r--r-- | packages/anastasis-webui/src/pages/home/index.tsx | 2 | ||||
-rw-r--r-- | packages/anastasis-webui/src/stories.tsx | 381 | ||||
-rw-r--r-- | packages/anastasis-webui/src/utils/index.tsx | 26 | ||||
-rw-r--r-- | packages/anastasis-webui/stories.html | 72 |
8 files changed, 639 insertions, 12 deletions
diff --git a/packages/anastasis-webui/clean_and_build.sh b/packages/anastasis-webui/clean_and_build.sh index 3da450c6b..85047b6e1 100755 --- a/packages/anastasis-webui/clean_and_build.sh +++ b/packages/anastasis-webui/clean_and_build.sh @@ -11,7 +11,7 @@ cp \ echo css pnpm exec sass -I . ./src/scss/main.scss dist/main.css & echo js -pnpm exec esbuild --log-level=error --bundle src/main.ts --outdir=dist --target=es6 --loader:.scss=text --loader:.svg=dataurl --format=iife --sourcemap --jsx-factory=h --jsx-fragment=Fragment --platform=browser & +pnpm exec esbuild --log-level=error --bundle src/main.ts --outdir=dist --target=es6 --loader:.svg=dataurl --format=iife --sourcemap --jsx-factory=h --jsx-fragment=Fragment --platform=browser & wait -n wait -n diff --git a/packages/anastasis-webui/dev.mjs b/packages/anastasis-webui/dev.mjs new file mode 100755 index 000000000..d6b6bf10d --- /dev/null +++ b/packages/anastasis-webui/dev.mjs @@ -0,0 +1,83 @@ +#!/usr/bin/env node +/* eslint-disable no-undef */ +import esbuild from 'esbuild' +import fs from 'fs'; +import WebSocket from "ws"; +import chokidar from "chokidar"; + +const devServerBroadcastDelay = 500 +const devServerPort = 8002 +const wss = new WebSocket.Server({ port: devServerPort }); +const toWatch = ["./src"] + +function broadcast(file, event) { + setTimeout(() => { + wss.clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + console.log(new Date(), file) + client.send(JSON.stringify(event)); + } + }); + }, devServerBroadcastDelay); +} + +const watcher = chokidar + .watch(toWatch, { + persistent: true, + ignoreInitial: true, + awaitWriteFinish: { + stabilityThreshold: 100, + pollInterval: 100, + }, + }) + .on("error", (error) => console.error(error)) + .on("change", async (file) => { + broadcast(file, { type: "RELOAD" }); + }) + .on("add", async (file) => { + broadcast(file, { type: "RELOAD" }); + }) + .on("unlink", async (file) => { + broadcast(file, { type: "RELOAD" }); + }); + + +fs.writeFileSync("dist/stories.html", fs.readFileSync("stories.html")) +fs.writeFileSync("dist/mocha.css", fs.readFileSync("node_modules/mocha/mocha.css")) +fs.writeFileSync("dist/mocha.js", fs.readFileSync("node_modules/mocha/mocha.js")) +fs.writeFileSync("dist/mocha.js.map", fs.readFileSync("node_modules/mocha/mocha.js.map")) + +export const buildConfig = { + entryPoints: ['src/stories.tsx'], + bundle: true, + outdir: 'dist', + minify: false, + loader: { + '.svg': 'dataurl', + }, + target: [ + 'es6' + ], + format: 'iife', + platform: 'browser', + sourcemap: true, + jsxFactory: 'h', + jsxFragment: 'Fragment', +} + +const server = await esbuild + .serve({ servedir: 'dist' }, { + ...buildConfig, outdir: 'dist' + }) + .catch((e) => { + console.log(e) + process.exit(1) + }); + +console.log(`Dev server is ready at http://localhost:${server.port}/. +http://localhost:${server.port}/stories.html for the components stories. +The server is running a using websocket at ${devServerPort} to notify code change and live reload. +`); + + + diff --git a/packages/anastasis-webui/package.json b/packages/anastasis-webui/package.json index a855ffa94..0cdb1be71 100644 --- a/packages/anastasis-webui/package.json +++ b/packages/anastasis-webui/package.json @@ -29,12 +29,15 @@ "@gnu-taler/anastasis-core": "workspace:*", "@gnu-taler/taler-util": "workspace:*", "base64-inline-loader": "1.1.1", + "chokidar": "^3.5.3", "date-fns": "2.28.0", "jed": "1.1.1", + "mocha": "^9.2.0", "preact": "^10.5.15", "preact-render-to-string": "^5.1.19", "preact-router": "^3.2.1", - "qrcode-generator": "^1.4.4" + "qrcode-generator": "^1.4.4", + "ws": "7.4.5" }, "devDependencies": { "@creativebulma/bulma-tooltip": "^1.2.0", diff --git a/packages/anastasis-webui/src/pages/home/index.storiesNo.tsx b/packages/anastasis-webui/src/pages/home/index.storiesNo.tsx new file mode 100644 index 000000000..5355b6c1a --- /dev/null +++ b/packages/anastasis-webui/src/pages/home/index.storiesNo.tsx @@ -0,0 +1,80 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import * as a1 from "./RecoveryFinishedScreen.stories.js"; +import * as a3 from "./ContinentSelectionScreen.stories.js"; +import * as a4 from "./ReviewPoliciesScreen.stories.js"; +import * as a5 from "./authMethod/AuthMethodSmsSolve.stories.js"; +import * as a6 from "./authMethod/AuthMethodSmsSetup.stories.js"; +import * as a7 from "./authMethod/AuthMethodPostSetup.stories.js"; +import * as a8 from "./authMethod/AuthMethodEmailSetup.stories.js"; +import * as a9 from "./authMethod/AuthMethodIbanSetup.stories.js"; +import * as a10 from "./authMethod/AuthMethodQuestionSolve.stories.js"; +import * as a11 from "./authMethod/AuthMethodIbanSolve.stories.js"; +import * as a12 from "./authMethod/AuthMethodTotpSolve.stories.js"; +import * as a13 from "./authMethod/AuthMethodPostSolve.stories.js"; +import * as a14 from "./authMethod/AuthMethodTotpSetup.stories.js"; +import * as a15 from "./authMethod/AuthMethodEmailSolve.stories.js"; +import * as a16 from "./authMethod/AuthMethodQuestionSetup.stories.js"; +import * as a17 from "./ChallengePayingScreen.stories.js"; +import * as a18 from "./AuthenticationEditorScreen.stories.js"; +import * as a19 from "./SecretSelectionScreen.stories.js"; +import * as a20 from "./PoliciesPayingScreen.stories.js"; +import * as a21 from "./BackupFinishedScreen.stories.js"; +import * as a22 from "./SecretEditorScreen.stories.js"; +import * as a23 from "./AddingProviderScreen.stories.js"; +import * as a24 from "./StartScreen.stories.js"; +import * as a25 from "./ChallengeOverviewScreen.stories.js"; +import * as a26 from "./TruthsPayingScreen.stories.js"; +import * as a27 from "./EditPoliciesScreen.stories.js"; +import * as a28 from "./AttributeEntryScreen.stories.js"; +import * as a29 from "./SolveScreen.stories.js"; + +export default [ + a1, + a3, + a4, + a5, + a6, + a7, + a8, + a9, + a10, + a11, + a12, + a13, + a14, + a15, + a16, + a17, + a18, + a19, + a20, + a21, + a22, + a23, + a24, + a25, + a26, + a27, + a28, + a29, +]; diff --git a/packages/anastasis-webui/src/pages/home/index.tsx b/packages/anastasis-webui/src/pages/home/index.tsx index 03bf21577..47b62c7e8 100644 --- a/packages/anastasis-webui/src/pages/home/index.tsx +++ b/packages/anastasis-webui/src/pages/home/index.tsx @@ -157,7 +157,6 @@ export function AnastasisClientFrame(props: AnastasisClientFrameProps): VNode { return ( <Fragment> - <Menu title="Anastasis" /> <div class="home" onKeyPress={(e) => handleKeyPress(e)}> <h1 class="title">{props.title}</h1> <ErrorBanner /> @@ -195,6 +194,7 @@ const AnastasisClient: FunctionalComponent = () => { return ( <AnastasisProvider value={reducer}> <ErrorBoundary reducer={reducer}> + <Menu title="Anastasis" /> <AnastasisClientImpl /> </ErrorBoundary> </AnastasisProvider> diff --git a/packages/anastasis-webui/src/stories.tsx b/packages/anastasis-webui/src/stories.tsx new file mode 100644 index 000000000..2b830766f --- /dev/null +++ b/packages/anastasis-webui/src/stories.tsx @@ -0,0 +1,381 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { setupI18n } from "@gnu-taler/taler-util"; +import { ComponentChild, Fragment, h, render, VNode } from "preact"; +import { useEffect, useErrorBoundary, useState } from "preact/hooks"; +import { strings } from "./i18n/strings.js"; +import * as pages from "./pages/home/index.storiesNo.js"; + +const url = new URL(window.location.href); +const lang = url.searchParams.get("lang") || "en"; + +setupI18n(lang, strings); + +const Page = ({ children }: any) => <div class="page">{children}</div>; +const SideBar = ({ children }: any) => <div class="sidebar">{children}</div>; +const Content = ({ children }: any) => <div class="content">{children}</div>; + +function parseExampleImport(group: string, im: any): ComponentItem { + const component = im.default.title; + const order: number = im.default.args?.order || 0; + return { + name: component, + order, + examples: Object.entries(im) + .filter(([k]) => k !== "default") + .map( + ([name, render]) => + ({ + group, + component, + name, + render, + } as ExampleItem), + ), + }; +} + +function SortStories(a: any, b: any): number { + return (a?.order ?? 0) - (b?.order ?? 0); +} + +const allExamples = Object.entries({ pages }).map(([title, value]) => ({ + title, + list: value.default + .map((s) => parseExampleImport(title, s)) + .sort(SortStories), +})); + +interface ComponentItem { + name: string; + order: number; + examples: ExampleItem[]; +} + +interface ExampleItem { + group: string; + component: string; + name: string; + render: { + (args: any): VNode; + args: any; + }; +} + +function findByGroupComponentName( + group: string, + component: string, + name: string, +): ExampleItem | undefined { + const gl = allExamples.filter((e) => e.title === group); + if (gl.length === 0) { + return undefined; + } + const cl = gl[0].list.filter((l) => l.name === component); + if (cl.length === 0) { + return undefined; + } + const el = cl[0].examples.filter((c) => c.name === name); + if (el.length === 0) { + return undefined; + } + return el[0]; +} + +function getContentForExample(item: ExampleItem | undefined): () => VNode { + if (!item) + return function SelectExampleMessage() { + return <div>select example from the list on the left</div>; + }; + const example = findByGroupComponentName( + item.group, + item.component, + item.name, + ); + if (!example) + return function ExampleNotFoundMessage() { + return <div>example not found</div>; + }; + return () => example.render(example.render.args); +} + +function ExampleList({ + name, + list, + selected, + onSelectStory, +}: { + name: string; + list: { + name: string; + examples: ExampleItem[]; + }[]; + selected: ExampleItem | undefined; + onSelectStory: (i: ExampleItem, id: string) => void; +}): VNode { + const [isOpen, setOpen] = useState(selected && selected.group === name); + return ( + <ol> + <div onClick={() => setOpen(!isOpen)}>{name}</div> + <div data-hide={!isOpen}> + {list.map((k) => ( + <li key={k.name}> + <dl> + <dt>{k.name}</dt> + {k.examples.map((r) => { + const e = encodeURIComponent; + const eId = `${e(r.group)}-${e(r.component)}-${e(r.name)}`; + const isSelected = + selected && + selected.component === r.component && + selected.group === r.group && + selected.name === r.name; + return ( + <dd id={eId} key={r.name} data-selected={isSelected}> + <a + href={`#${eId}`} + onClick={(e) => { + e.preventDefault(); + location.hash = `#${eId}`; + onSelectStory(r, eId); + }} + > + {r.name} + </a> + </dd> + ); + })} + </dl> + </li> + ))} + </div> + </ol> + ); +} + +// function getWrapperForGroup(group: string): FunctionComponent { +// switch (group) { +// case "popup": +// return function PopupWrapper({ children }: any) { +// return ( +// <Fragment> +// <PopupNavBar /> +// <PopupBox>{children}</PopupBox> +// </Fragment> +// ); +// }; +// case "wallet": +// return function WalletWrapper({ children }: any) { +// return ( +// <Fragment> +// <LogoHeader /> +// <WalletNavBar /> +// <WalletBox>{children}</WalletBox> +// </Fragment> +// ); +// }; +// case "cta": +// return function WalletWrapper({ children }: any) { +// return ( +// <Fragment> +// <WalletBox>{children}</WalletBox> +// </Fragment> +// ); +// }; +// default: +// return Fragment; +// } +// } + +function ErrorReport({ + children, + selected, +}: { + children: ComponentChild; + selected: ExampleItem | undefined; +}): VNode { + const [error] = useErrorBoundary(); + if (error) { + return ( + <div class="error_report"> + <p>Error was thrown trying to render</p> + {selected && ( + <ul> + <li> + <b>group</b>: {selected.group} + </li> + <li> + <b>component</b>: {selected.component} + </li> + <li> + <b>example</b>: {selected.name} + </li> + <li> + <b>args</b>:{" "} + <pre>{JSON.stringify(selected.render.args, undefined, 2)}</pre> + </li> + </ul> + )} + <p>{error.message}</p> + <pre>{error.stack}</pre> + </div> + ); + } + return <Fragment>{children}</Fragment>; +} + +function getSelectionFromLocationHash(hash: string): ExampleItem | undefined { + if (!hash) return undefined; + const parts = hash.substring(1).split("-"); + if (parts.length < 3) return undefined; + return findByGroupComponentName( + decodeURIComponent(parts[0]), + decodeURIComponent(parts[1]), + decodeURIComponent(parts[2]), + ); +} + +function Application(): VNode { + const initialSelection = getSelectionFromLocationHash(location.hash); + const [selected, updateSelected] = useState<ExampleItem | undefined>( + initialSelection, + ); + useEffect(() => { + if (location.hash) { + const hash = location.hash.substring(1); + const found = document.getElementById(hash); + if (found) { + setTimeout(() => { + found.scrollIntoView({ + block: "center", + }); + }, 10); + } + } + }, []); + + const ExampleContent = getContentForExample(selected); + + // const GroupWrapper = getWrapperForGroup(selected?.group || "default"); + + return ( + <Page> + <LiveReload /> + <SideBar> + {allExamples.map((e) => ( + <ExampleList + key={e.title} + name={e.title} + list={e.list} + selected={selected} + onSelectStory={(item, htmlId) => { + document.getElementById(htmlId)?.scrollIntoView({ + block: "center", + }); + updateSelected(item); + }} + /> + ))} + <hr /> + </SideBar> + <Content> + <ErrorReport selected={selected}> + {/* <GroupWrapper> */} + <ExampleContent /> + {/* </GroupWrapper> */} + </ErrorReport> + </Content> + </Page> + ); +} + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", main); +} else { + main(); +} +function main(): void { + try { + const container = document.getElementById("container"); + if (!container) { + throw Error("container not found, can't mount page contents"); + } + render(<Application />, container); + } catch (e) { + console.error("got error", e); + if (e instanceof Error) { + document.body.innerText = `Fatal error: "${e.message}". Please report this bug at https://bugs.gnunet.org/.`; + } + } +} + +let liveReloadMounted = false; +function LiveReload({ port = 8002 }: { port?: number }): VNode { + const [isReloading, setIsReloading] = useState(false); + useEffect(() => { + if (!liveReloadMounted) { + setupLiveReload(port, () => { + setIsReloading(true); + window.location.reload(); + }); + liveReloadMounted = true; + } + }); + + if (isReloading) { + return ( + <div + style={{ + position: "absolute", + width: "100%", + height: "100%", + backgroundColor: "rgba(0,0,0,0.5)", + color: "white", + display: "flex", + justifyContent: "center", + }} + > + <h1 style={{ margin: "auto" }}>reloading...</h1> + </div> + ); + } + return <Fragment />; +} + +function setupLiveReload(port: number, onReload: () => void): void { + const protocol = location.protocol === "https:" ? "wss:" : "ws:"; + const host = location.hostname; + const socketPath = `${protocol}//${host}:${port}/socket`; + + const ws = new WebSocket(socketPath); + ws.onmessage = (message) => { + const event = JSON.parse(message.data); + if (event.type === "LOG") { + console.log(event.message); + } + if (event.type === "RELOAD") { + onReload(); + } + }; + ws.onerror = (error) => { + console.error(error); + }; +} diff --git a/packages/anastasis-webui/src/utils/index.tsx b/packages/anastasis-webui/src/utils/index.tsx index 4dace0934..2e502cacb 100644 --- a/packages/anastasis-webui/src/utils/index.tsx +++ b/packages/anastasis-webui/src/utils/index.tsx @@ -1,5 +1,8 @@ /* eslint-disable @typescript-eslint/camelcase */ import { + AuthenticationProviderStatus, + AuthenticationProviderStatusError, + AuthenticationProviderStatusOk, BackupStates, RecoveryStates, ReducerState, @@ -115,6 +118,7 @@ const base = { ], authentication_providers: { "http://localhost:8086/": { + status: "ok", http_status: 200, annual_fee: "COL:0", business_name: "Anastasis Local", @@ -134,11 +138,12 @@ const base = { usage_fee: "COL:0", }, ], - salt: "WBMDD76BR1E90YQ5AHBMKPH7GW", + provider_salt: "WBMDD76BR1E90YQ5AHBMKPH7GW", storage_limit_in_megabytes: 16, truth_upload_fee: "COL:0", - }, + } as AuthenticationProviderStatusOk, "https://kudos.demo.anastasis.lu/": { + status: "ok", http_status: 200, annual_fee: "COL:0", business_name: "Anastasis Kudo", @@ -154,11 +159,12 @@ const base = { usage_fee: "COL:0", }, ], - salt: "WBMDD76BR1E90YQ5AHBMKPH7GW", + provider_salt: "WBMDD76BR1E90YQ5AHBMKPH7GW", storage_limit_in_megabytes: 16, truth_upload_fee: "COL:0", - }, + } as AuthenticationProviderStatusOk, "https://anastasis.demo.taler.net/": { + status: "ok", http_status: 200, annual_fee: "COL:0", business_name: "Anastasis Demo", @@ -178,23 +184,23 @@ const base = { usage_fee: "COL:0", }, ], - salt: "WBMDD76BR1E90YQ5AHBMKPH7GW", + provider_salt: "WBMDD76BR1E90YQ5AHBMKPH7GW", storage_limit_in_megabytes: 16, truth_upload_fee: "COL:0", - }, + } as AuthenticationProviderStatusOk, "http://localhost:8087/": { code: 8414, hint: "request to provider failed", - }, + } as AuthenticationProviderStatusError, "http://localhost:8088/": { code: 8414, hint: "request to provider failed", - }, + } as AuthenticationProviderStatusError, "http://localhost:8089/": { code: 8414, hint: "request to provider failed", - }, + } as AuthenticationProviderStatusError, }, } as Partial<ReducerState>; @@ -210,6 +216,7 @@ export const reducerStatesExample = { } as ReducerState, secretSelection: { ...base, + reducer_type: "recovery", recovery_state: RecoveryStates.SecretSelecting, } as ReducerState, recoveryFinished: { @@ -260,6 +267,7 @@ export const reducerStatesExample = { authEditing: { ...base, backup_state: BackupStates.AuthenticationsEditing, + reducer_type: "backup", } as ReducerState, backupAttributeEditing: { ...base, diff --git a/packages/anastasis-webui/stories.html b/packages/anastasis-webui/stories.html new file mode 100644 index 000000000..9f41fdeaf --- /dev/null +++ b/packages/anastasis-webui/stories.html @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<html> + <head> + <title>Stories</title> + <style> + /* page css */ + div.page { + margin: 0px; + padding: 0px; + font-size: 100%; + font-family: Arial, Helvetica, sans-serif; + } + div.page p:not([class]) { + margin-bottom: 1em; + margin-top: 1em; + } + div.page { + width: 100%; + display: flex; + flex-direction: row; + } + /* sidebar css */ + div.sidebar { + min-width: 200px; + height: calc(100vh - 20px); + overflow-y: visible; + overflow-x: hidden; + scroll-behavior: smooth; + } + div.sidebar > ol { + padding: 4px; + } + div.sidebar div:first-child { + background-color: lightcoral; + cursor: pointer; + } + div.sidebar div[data-hide="true"] { + display: none; + } + div.sidebar dd { + margin-left: 1em; + padding: 4px; + cursor: pointer; + border-radius: 4px; + margin-bottom: 4px; + } + div.sidebar dd:nth-child(even) { + background-color: lightgray; + } + div.sidebar dd:nth-child(odd) { + background-color: lightblue; + } + div.sidebar a { + color: black; + } + div.sidebar dd[data-selected] { + background-color: green; + } + + /* content css */ + div.content { + width: 100%; + padding: 20px; + } + </style> + <script src="./stories.js"></script> + <link rel="stylesheet" href="./main.css" /> + </head> + <body> + <taler-stories id="container"></taler-stories> + </body> +</html> |