diff options
author | Sebastian <sebasjm@gmail.com> | 2022-06-11 19:10:26 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2022-06-11 19:10:26 -0300 |
commit | 6d06b52605005f4d25381fc73383c3c9e48f20f8 (patch) | |
tree | d1e01d71c538602a92848595f92d24bf214c264f /packages/anastasis-webui | |
parent | 716da3246b7d544fc81265d1942ae64067ecd8b7 (diff) |
add testing to web components
Diffstat (limited to 'packages/anastasis-webui')
16 files changed, 779 insertions, 439 deletions
diff --git a/packages/anastasis-webui/clean_and_build.sh b/packages/anastasis-webui/clean_and_build.sh index 21107a905..c0e93e47d 100755 --- a/packages/anastasis-webui/clean_and_build.sh +++ b/packages/anastasis-webui/clean_and_build.sh @@ -37,6 +37,8 @@ echo compile build_css & build_js src/main.ts & build_js src/main.test.ts & +for file in $(find src/ -name test.ts); do build_js $file; done & +wait -n wait -n wait -n wait -n diff --git a/packages/anastasis-webui/package.json b/packages/anastasis-webui/package.json index 7c57160ce..551ae54bc 100644 --- a/packages/anastasis-webui/package.json +++ b/packages/anastasis-webui/package.json @@ -9,12 +9,14 @@ "dev": "./dev.mjs", "prepare": "pnpm compile", "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'", - "test": "mocha --enable-source-maps 'dist/**/*.test.js'", + "test": "mocha --enable-source-maps 'dist/**/*test.js'", "pretty": "prettier --write src" }, "dependencies": { "@gnu-taler/anastasis-core": "workspace:*", "@gnu-taler/taler-util": "workspace:*", + "@types/chai": "^4.3.0", + "chai": "^4.3.6", "date-fns": "2.28.0", "jed": "1.1.1", "preact": "^10.5.15", diff --git a/packages/anastasis-webui/src/components/InvalidState.tsx b/packages/anastasis-webui/src/components/InvalidState.tsx new file mode 100644 index 000000000..8e2edde5e --- /dev/null +++ b/packages/anastasis-webui/src/components/InvalidState.tsx @@ -0,0 +1,21 @@ +/* + This file is part of GNU Anastasis + (C) 2021-2022 Anastasis SARL + + GNU Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Anastasis 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { h, VNode } from "preact"; + +export default function InvalidState(): VNode { + return <div>invalid state</div>; +} diff --git a/packages/anastasis-webui/src/components/NoReducer.tsx b/packages/anastasis-webui/src/components/NoReducer.tsx new file mode 100644 index 000000000..550ddccaa --- /dev/null +++ b/packages/anastasis-webui/src/components/NoReducer.tsx @@ -0,0 +1,21 @@ +/* + This file is part of GNU Anastasis + (C) 2021-2022 Anastasis SARL + + GNU Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Anastasis 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { h, VNode } from "preact"; + +export default function NoReducer(): VNode { + return <div>no reducer</div>; +} diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/AddingProviderScreen.stories.tsx deleted file mode 100644 index ce860ba29..000000000 --- a/packages/anastasis-webui/src/pages/home/AddingProviderScreen.stories.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/* - This file is part of GNU Anastasis - (C) 2021-2022 Anastasis SARL - - GNU Anastasis is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Anastasis 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 Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License along with - GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { ReducerState } from "@gnu-taler/anastasis-core"; -import { createExample, reducerStatesExample } from "../../utils/index.js"; -import { AddingProviderScreen as TestedComponent } from "./AddingProviderScreen.js"; - -export default { - title: "Pages/ManageProvider", - component: TestedComponent, - args: { - order: 1, - }, - argTypes: { - onUpdate: { action: "onUpdate" }, - onBack: { action: "onBack" }, - }, -}; - -export const NewProvider = createExample(TestedComponent, { - ...reducerStatesExample.authEditing, -} as ReducerState); - -export const NewProviderWithoutProviderList = createExample(TestedComponent, { - ...reducerStatesExample.authEditing, - authentication_providers: {}, -} as ReducerState); - -export const NewSmsProvider = createExample( - TestedComponent, - { - ...reducerStatesExample.authEditing, - } as ReducerState, - { providerType: "sms" }, -); - -export const NewIBANProvider = createExample( - TestedComponent, - { - ...reducerStatesExample.authEditing, - } as ReducerState, - { providerType: "iban" }, -); diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx b/packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx deleted file mode 100644 index 6aeee9e7a..000000000 --- a/packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx +++ /dev/null @@ -1,367 +0,0 @@ -/* - This file is part of GNU Anastasis - (C) 2021-2022 Anastasis SARL - - GNU Anastasis is free software; you can redistribute it and/or modify it under the - terms of the GNU Affero General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Anastasis 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 Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License along with - GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -import { - AuthenticationProviderStatus, - AuthenticationProviderStatusError, - AuthenticationProviderStatusOk, -} from "@gnu-taler/anastasis-core"; -import { h, VNode } from "preact"; -import { useEffect, useRef, useState } from "preact/hooks"; -import { TextInput } from "../../components/fields/TextInput.js"; -import { useAnastasisContext } from "../../context/anastasis.js"; -import { authMethods, KnownAuthMethods } from "./authMethod/index.js"; -import { AnastasisClientFrame } from "./index.js"; - -interface Props { - providerType?: KnownAuthMethods; - onCancel: () => void; -} - -async function testProvider( - url: string, - expectedMethodType?: string, -): Promise<void> { - try { - const response = await fetch(new URL("config", url).href); - const json = await response.json().catch((d) => ({})); - if (!("methods" in json) || !Array.isArray(json.methods)) { - throw Error( - "This provider doesn't have authentication method. Check the provider URL", - ); - } - if (!expectedMethodType) { - return; - } - let found = false; - for (let i = 0; i < json.methods.length && !found; i++) { - found = json.methods[i].type === expectedMethodType; - } - if (!found) { - throw Error( - `This provider does not support authentication method ${expectedMethodType}`, - ); - } - return; - } catch (e) { - console.log("error", e); - const error = - e instanceof Error - ? Error( - `There was an error testing this provider, try another one. ${e.message}`, - ) - : Error(`There was an error testing this provider, try another one.`); - throw error; - } -} - -export function AddingProviderScreen({ providerType, onCancel }: Props): VNode { - const reducer = useAnastasisContext(); - - const [providerURL, setProviderURL] = useState(""); - - const [error, setError] = useState<string | undefined>(); - const [testing, setTesting] = useState(false); - - const providerLabel = providerType - ? authMethods[providerType].label - : undefined; - - const allAuthProviders = - !reducer || - !reducer.currentReducerState || - reducer.currentReducerState.reducer_type === "error" || - !reducer.currentReducerState.authentication_providers - ? {} - : reducer.currentReducerState.authentication_providers; - - const authProvidersByStatus = Object.keys(allAuthProviders).reduce( - (prev, url) => { - const p = allAuthProviders[url]; - if ( - providerLabel && - p.status === "ok" && - p.methods.findIndex((m) => m.type === providerType) !== -1 - ) { - return prev; - } - const others = prev[p.status] ? prev[p.status] : []; - others.push({ ...p, url }); - return { - ...prev, - [p.status]: others, - }; - }, - {} as Record< - AuthenticationProviderStatus["status"], - (AuthenticationProviderStatus & { url: string })[] - >, - ); - const authProviders = authProvidersByStatus["ok"].map((p) => p.url); - - console.log("rodos", allAuthProviders); - //FIXME: move this timeout logic into a hook - const timeout = useRef<number | undefined>(undefined); - useEffect(() => { - if (timeout) window.clearTimeout(timeout.current); - timeout.current = window.setTimeout(async () => { - const url = providerURL.endsWith("/") ? providerURL : providerURL + "/"; - if (!providerURL || authProviders.includes(url)) return; - try { - setTesting(true); - await testProvider(url, providerType); - setError(""); - } catch (e) { - if (e instanceof Error) setError(e.message); - } - setTesting(false); - }, 200); - }, [providerURL, reducer]); - - async function addProvider(provider_url: string): Promise<void> { - await reducer?.transition("add_provider", { provider_url }); - onCancel(); - } - function deleteProvider(provider_url: string): void { - reducer?.transition("delete_provider", { provider_url }); - } - - if (!reducer) { - return <div>no reducer in context</div>; - } - - if ( - !reducer.currentReducerState || - !("authentication_providers" in reducer.currentReducerState) - ) { - return <div>invalid state</div>; - } - - let errors = !providerURL ? "Add provider URL" : undefined; - let url: string | undefined; - try { - url = new URL("", providerURL).href; - } catch { - errors = "Check the URL"; - } - if (!!error && !errors) { - errors = error; - } - if (!errors && authProviders.includes(url!)) { - errors = "That provider is already known"; - } - - return ( - <AnastasisClientFrame - hideNav - title="Backup: Manage providers" - hideNext={errors} - > - <div> - {!providerLabel ? ( - <p>Add a provider url</p> - ) : ( - <p>Add a provider url for a {providerLabel} service</p> - )} - <div class="container"> - <TextInput - label="Provider URL" - placeholder="https://provider.com" - grabFocus - error={errors} - bind={[providerURL, setProviderURL]} - /> - </div> - <p class="block">Example: https://kudos.demo.anastasis.lu</p> - {testing && <p class="has-text-info">Testing</p>} - - <div - class="block" - style={{ - marginTop: "2em", - display: "flex", - justifyContent: "space-between", - }} - > - <button class="button" onClick={onCancel}> - Cancel - </button> - <span data-tooltip={errors}> - <button - class="button is-info" - disabled={error !== "" || testing} - onClick={() => addProvider(url!)} - > - Add - </button> - </span> - </div> - - {authProviders.length > 0 ? ( - !providerLabel ? ( - <p class="subtitle">Current providers</p> - ) : ( - <p class="subtitle"> - Current providers for {providerLabel} service - </p> - ) - ) : !providerLabel ? ( - <p class="subtitle">No known providers, add one.</p> - ) : ( - <p class="subtitle">No known providers for {providerLabel} service</p> - )} - - {authProviders.map((k) => { - const p = allAuthProviders[k] as AuthenticationProviderStatusOk; - return ( - <TableRow key={k} url={k} info={p} onDelete={deleteProvider} /> - ); - })} - {authProvidersByStatus["error"]?.map((k) => { - const p = k as AuthenticationProviderStatusError; - return ( - <TableRowError - key={k} - url={k.url} - info={p} - onDelete={deleteProvider} - /> - ); - })} - </div> - </AnastasisClientFrame> - ); -} -function TableRow({ - url, - info, - onDelete, -}: { - onDelete: (s: string) => void; - url: string; - info: AuthenticationProviderStatusOk; -}): VNode { - const [status, setStatus] = useState("checking"); - useEffect(function () { - testProvider(url.endsWith("/") ? url.substring(0, url.length - 1) : url) - .then(function () { - setStatus("responding"); - }) - .catch(function () { - setStatus("failed to contact"); - }); - }); - return ( - <div - class="box" - style={{ display: "flex", justifyContent: "space-between" }} - > - <div> - <div class="subtitle">{url}</div> - <dl> - <dt> - <b>Business Name</b> - </dt> - <dd>{info.business_name}</dd> - <dt> - <b>Supported methods</b> - </dt> - <dd>{info.methods.map((m) => m.type).join(",")}</dd> - <dt> - <b>Maximum storage</b> - </dt> - <dd>{info.storage_limit_in_megabytes} Mb</dd> - <dt> - <b>Status</b> - </dt> - <dd>{status}</dd> - </dl> - </div> - <div - class="block" - style={{ - marginTop: "auto", - marginBottom: "auto", - display: "flex", - justifyContent: "space-between", - flexDirection: "column", - }} - > - <button class="button is-danger" onClick={() => onDelete(url)}> - Remove - </button> - </div> - </div> - ); -} - -function TableRowError({ - url, - info, - onDelete, -}: { - onDelete: (s: string) => void; - url: string; - info: AuthenticationProviderStatusError; -}): VNode { - const [status, setStatus] = useState("checking"); - useEffect(function () { - testProvider(url.endsWith("/") ? url.substring(0, url.length - 1) : url) - .then(function () { - setStatus("responding"); - }) - .catch(function () { - setStatus("failed to contact"); - }); - }); - return ( - <div - class="box" - style={{ display: "flex", justifyContent: "space-between" }} - > - <div> - <div class="subtitle">{url}</div> - <dl> - <dt> - <b>Error</b> - </dt> - <dd>{info.hint}</dd> - <dt> - <b>Code</b> - </dt> - <dd>{info.code}</dd> - <dt> - <b>Status</b> - </dt> - <dd>{status}</dd> - </dl> - </div> - <div - class="block" - style={{ - marginTop: "auto", - marginBottom: "auto", - display: "flex", - justifyContent: "space-between", - flexDirection: "column", - }} - > - <button class="button is-danger" onClick={() => onDelete(url)}> - Remove - </button> - </div> - </div> - ); -} diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/index.ts b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/index.ts new file mode 100644 index 000000000..5d5913ffc --- /dev/null +++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/index.ts @@ -0,0 +1,102 @@ +/* + This file is part of GNU Anastasis + (C) 2021-2022 Anastasis SARL + + GNU Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Anastasis 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { AuthenticationProviderStatus } from "@gnu-taler/anastasis-core"; +import InvalidState from "../../../components/InvalidState.js"; +import NoReducer from "../../../components/NoReducer.js"; +import { compose, StateViewMap } from "../../../utils/index.js"; +import useComponentState from "./state.js"; +import { WithoutProviderType, WithProviderType } from "./views.js"; + +export type AuthProvByStatusMap = Record< + AuthenticationProviderStatus["status"], + (AuthenticationProviderStatus & { url: string })[] +> + +export type State = NoReducer | InvalidState | WithType | WithoutType; + +export interface NoReducer { + status: "no-reducer"; +} +export interface InvalidState { + status: "invalid-state"; +} + +interface CommonProps { + addProvider?: () => Promise<void>; + deleteProvider: (url: string) => Promise<void>; + authProvidersByStatus: AuthProvByStatusMap; + error: string | undefined; + onCancel: () => Promise<void>; + testing: boolean; + setProviderURL: (url: string) => Promise<void>; + providerURL: string; + errors: string | undefined; +} + +export interface WithType extends CommonProps { + status: "with-type"; + providerLabel: string; +} +export interface WithoutType extends CommonProps { + status: "without-type"; +} + +const map: StateViewMap<State> = { + "no-reducer": NoReducer, + "invalid-state": InvalidState, + "with-type": WithProviderType, + "without-type": WithoutProviderType, +}; + +export default compose("AddingProviderScreen", useComponentState, map) + + +export async function testProvider( + url: string, + expectedMethodType?: string, +): Promise<void> { + try { + const response = await fetch(new URL("config", url).href); + const json = await response.json().catch((d) => ({})); + if (!("methods" in json) || !Array.isArray(json.methods)) { + throw Error( + "This provider doesn't have authentication method. Check the provider URL", + ); + } + if (!expectedMethodType) { + return; + } + let found = false; + for (let i = 0; i < json.methods.length && !found; i++) { + found = json.methods[i].type === expectedMethodType; + } + if (!found) { + throw Error( + `This provider does not support authentication method ${expectedMethodType}`, + ); + } + return; + } catch (e) { + console.log("error", e); + const error = + e instanceof Error + ? Error( + `There was an error testing this provider, try another one. ${e.message}`, + ) + : Error(`There was an error testing this provider, try another one.`); + throw error; + } +} diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/state.ts b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/state.ts new file mode 100644 index 000000000..a04c7957b --- /dev/null +++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/state.ts @@ -0,0 +1,147 @@ +/* + This file is part of GNU Anastasis + (C) 2021-2022 Anastasis SARL + + GNU Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Anastasis 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { useEffect, useRef, useState } from "preact/hooks"; +import { useAnastasisContext } from "../../../context/anastasis.js"; +import { authMethods, KnownAuthMethods } from "../authMethod/index.jsx"; +import { AuthProvByStatusMap, State, testProvider } from "./index.js"; + +interface Props { + providerType?: KnownAuthMethods; + onCancel: () => Promise<void>; +} + +export default function useComponentState({ providerType, onCancel }: Props): State { + const reducer = useAnastasisContext(); + + const [providerURL, setProviderURL] = useState(""); + + const [error, setError] = useState<string | undefined>(); + const [testing, setTesting] = useState(false); + + const providerLabel = providerType + ? authMethods[providerType].label + : undefined; + + const allAuthProviders = + !reducer || + !reducer.currentReducerState || + reducer.currentReducerState.reducer_type === "error" || + !reducer.currentReducerState.authentication_providers + ? {} + : reducer.currentReducerState.authentication_providers; + + const authProvidersByStatus = Object.keys(allAuthProviders).reduce( + (prev, url) => { + const p = allAuthProviders[url]; + if ( + providerLabel && + p.status === "ok" && + p.methods.findIndex((m) => m.type === providerType) !== -1 + ) { + return prev; + } + prev[p.status].push({ ...p, url }); + return prev; + }, + { "not-contacted": [], disabled: [], error: [], ok: [] } as AuthProvByStatusMap, + ); + const authProviders = authProvidersByStatus["ok"].map((p) => p.url); + + //FIXME: move this timeout logic into a hook + const timeout = useRef<ReturnType<typeof setTimeout> | undefined>(undefined); + useEffect(() => { + if (timeout.current) clearTimeout(timeout.current); + timeout.current = setTimeout(async () => { + const url = providerURL.endsWith("/") ? providerURL : providerURL + "/"; + if (!providerURL || authProviders.includes(url)) return; + try { + setTesting(true); + await testProvider(url, providerType); + setError(""); + } catch (e) { + if (e instanceof Error) setError(e.message); + } + setTesting(false); + }, 200); + }, [providerURL, reducer]); + + if (!reducer) { + return { + status: "no-reducer", + }; + } + + if ( + !reducer.currentReducerState || + !("authentication_providers" in reducer.currentReducerState) + ) { + return { + status: "invalid-state", + }; + } + + const addProvider = async (provider_url: string): Promise<void> => { + await reducer.transition("add_provider", { provider_url }); + onCancel(); + } + const deleteProvider = async (provider_url: string): Promise<void> => { + reducer.transition("delete_provider", { provider_url }); + } + + let errors = !providerURL ? "Add provider URL" : undefined; + let url: string | undefined; + try { + url = new URL("", providerURL).href; + } catch { + errors = "Check the URL"; + } + const _url = url + + if (!!error && !errors) { + errors = error; + } + if (!errors && authProviders.includes(url!)) { + errors = "That provider is already known"; + } + + const commonState = { + addProvider: !_url ? undefined : async () => addProvider(_url), + deleteProvider: async (url: string) => deleteProvider(url), + allAuthProviders, + authProvidersByStatus, + onCancel, + providerURL, + testing, + setProviderURL: async (s: string) => setProviderURL(s), + errors, + error, + } + + if (!providerLabel) { + return { + status: "without-type", + ...commonState + } + } else { + return { + status: "with-type", + providerLabel, + ...commonState + } + } + +} + diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/stories.tsx b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/stories.tsx new file mode 100644 index 000000000..6d8a634d5 --- /dev/null +++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/stories.tsx @@ -0,0 +1,89 @@ +/* + This file is part of GNU Anastasis + (C) 2021-2022 Anastasis SARL + + GNU Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Anastasis 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { AuthenticationProviderStatusOk } from "@gnu-taler/anastasis-core"; +import { createExampleWithoutAnastasis } from "../../../utils/index.jsx"; +import { WithoutProviderType, WithProviderType } from "./views.jsx"; + +export default { + title: "Pages/ManageProvider", + args: { + order: 1, + }, + argTypes: { + onUpdate: { action: "onUpdate" }, + onBack: { action: "onBack" }, + }, +}; + +export const NewProvider = createExampleWithoutAnastasis(WithoutProviderType, { + authProvidersByStatus: { + ok: [ + { + business_name: "X provider", + status: "ok", + storage_limit_in_megabytes: 5, + methods: [ + { + type: "question", + usage_fee: "KUDOS:1", + }, + ], + url: "", + } as AuthenticationProviderStatusOk & { url: string }, + ], + "not-contacted": [], + disabled: [], + error: [], + }, +}); + +export const NewProviderWithoutProviderList = createExampleWithoutAnastasis( + WithoutProviderType, + { + authProvidersByStatus: { + ok: [], + "not-contacted": [], + disabled: [], + error: [], + }, + }, +); + +export const NewSmsProvider = createExampleWithoutAnastasis(WithProviderType, { + authProvidersByStatus: { + ok: [], + "not-contacted": [], + disabled: [], + error: [], + }, + providerLabel: "sms", +}); + +export const NewIBANProvider = createExampleWithoutAnastasis(WithProviderType, { + authProvidersByStatus: { + ok: [], + "not-contacted": [], + disabled: [], + error: [], + }, + providerLabel: "IBAN", +}); diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/test.ts b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/test.ts new file mode 100644 index 000000000..d051d7c0b --- /dev/null +++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/test.ts @@ -0,0 +1,42 @@ +/* + This file is part of GNU Anastasis + (C) 2021-2022 Anastasis SARL + + GNU Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Anastasis 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { expect } from "chai"; +import { mountHook } from "../../../test-utils.js"; +import useComponentState from "./state.js"; + +describe("AddingProviderScreen states", () => { + it("should have status 'no-balance' when balance is empty", async () => { + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = + mountHook(() => + useComponentState({ onCancel: async () => { null } }), + ); + + { + const { status } = getLastResultOrThrow(); + expect(status).equal("no-reducer"); + } + + await assertNoPendingUpdate(); + + }); + +}); diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen/views.tsx b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/views.tsx new file mode 100644 index 000000000..bb1283a82 --- /dev/null +++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen/views.tsx @@ -0,0 +1,304 @@ +/* + This file is part of GNU Anastasis + (C) 2021-2022 Anastasis SARL + + GNU Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Anastasis 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +import { + AuthenticationProviderStatusError, + AuthenticationProviderStatusOk, +} from "@gnu-taler/anastasis-core"; +import { h, VNode } from "preact"; +import { useEffect, useState } from "preact/hooks"; +import { TextInput } from "../../../components/fields/TextInput.js"; +import { AnastasisClientFrame } from "../index.js"; +import { testProvider, WithoutType, WithType } from "./index.js"; + +export function WithProviderType(props: WithType): VNode { + return ( + <AnastasisClientFrame + hideNav + title="Backup: Manage providers1" + hideNext={props.errors} + > + <div> + <p>Add a provider url for a {props.providerLabel} service</p> + <div class="container"> + <TextInput + label="Provider URL" + placeholder="https://provider.com" + grabFocus + error={props.errors} + bind={[props.providerURL, props.setProviderURL]} + /> + </div> + <p class="block">Example: https://kudos.demo.anastasis.lu</p> + {props.testing && <p class="has-text-info">Testing</p>} + + <div + class="block" + style={{ + marginTop: "2em", + display: "flex", + justifyContent: "space-between", + }} + > + <button class="button" onClick={props.onCancel}> + Cancel + </button> + <span data-tooltip={props.errors}> + <button + class="button is-info" + disabled={props.error !== "" || props.testing} + onClick={props.addProvider} + > + Add + </button> + </span> + </div> + + {props.authProvidersByStatus["ok"].length > 0 ? ( + <p class="subtitle"> + Current providers for {props.providerLabel} service + </p> + ) : ( + <p class="subtitle"> + No known providers for {props.providerLabel} service + </p> + )} + + {props.authProvidersByStatus["ok"].map((k, i) => { + const p = k as AuthenticationProviderStatusOk; + return ( + <TableRow + key={i} + url={k.url} + info={p} + onDelete={props.deleteProvider} + /> + ); + })} + <p class="subtitle">Providers with errors</p> + {props.authProvidersByStatus["error"].map((k, i) => { + const p = k as AuthenticationProviderStatusError; + return ( + <TableRowError + key={i} + url={k.url} + info={p} + onDelete={props.deleteProvider} + /> + ); + })} + </div> + </AnastasisClientFrame> + ); +} + +export function WithoutProviderType(props: WithoutType): VNode { + return ( + <AnastasisClientFrame + hideNav + title="Backup: Manage providers2" + hideNext={props.errors} + > + <div> + <p>Add a provider url</p> + <div class="container"> + <TextInput + label="Provider URL" + placeholder="https://provider.com" + grabFocus + error={props.errors} + bind={[props.providerURL, props.setProviderURL]} + /> + </div> + <p class="block">Example: https://kudos.demo.anastasis.lu</p> + {props.testing && <p class="has-text-info">Testing</p>} + + <div + class="block" + style={{ + marginTop: "2em", + display: "flex", + justifyContent: "space-between", + }} + > + <button class="button" onClick={props.onCancel}> + Cancel + </button> + <span data-tooltip={props.errors}> + <button + class="button is-info" + disabled={props.error !== "" || props.testing} + onClick={props.addProvider} + > + Add + </button> + </span> + </div> + + {props.authProvidersByStatus["ok"].length > 0 ? ( + <p class="subtitle">Current providers</p> + ) : ( + <p class="subtitle">No known providers, add one.</p> + )} + + {props.authProvidersByStatus["ok"].map((k, i) => { + const p = k as AuthenticationProviderStatusOk; + return ( + <TableRow + key={i} + url={k.url} + info={p} + onDelete={props.deleteProvider} + /> + ); + })} + <p class="subtitle">Providers with errors</p> + {props.authProvidersByStatus["error"].map((k, i) => { + const p = k as AuthenticationProviderStatusError; + return ( + <TableRowError + key={i} + url={k.url} + info={p} + onDelete={props.deleteProvider} + /> + ); + })} + </div> + </AnastasisClientFrame> + ); +} + +function TableRow({ + url, + info, + onDelete, +}: { + onDelete: (s: string) => Promise<void>; + url: string; + info: AuthenticationProviderStatusOk; +}): VNode { + const [status, setStatus] = useState("checking"); + useEffect(function () { + testProvider(url.endsWith("/") ? url.substring(0, url.length - 1) : url) + .then(function () { + setStatus("responding"); + }) + .catch(function () { + setStatus("failed to contact"); + }); + }); + return ( + <div + class="box" + style={{ display: "flex", justifyContent: "space-between" }} + > + <div> + <div class="subtitle">{url}</div> + <dl> + <dt> + <b>Business Name</b> + </dt> + <dd>{info.business_name}</dd> + <dt> + <b>Supported methods</b> + </dt> + <dd>{info.methods.map((m) => m.type).join(",")}</dd> + <dt> + <b>Maximum storage</b> + </dt> + <dd>{info.storage_limit_in_megabytes} Mb</dd> + <dt> + <b>Status</b> + </dt> + <dd>{status}</dd> + </dl> + </div> + <div + class="block" + style={{ + marginTop: "auto", + marginBottom: "auto", + display: "flex", + justifyContent: "space-between", + flexDirection: "column", + }} + > + <button class="button is-danger" onClick={() => onDelete(url)}> + Remove + </button> + </div> + </div> + ); +} + +function TableRowError({ + url, + info, + onDelete, +}: { + onDelete: (s: string) => void; + url: string; + info: AuthenticationProviderStatusError; +}): VNode { + const [status, setStatus] = useState("checking"); + useEffect(function () { + testProvider(url.endsWith("/") ? url.substring(0, url.length - 1) : url) + .then(function () { + setStatus("responding"); + }) + .catch(function () { + setStatus("failed to contact"); + }); + }); + return ( + <div + class="box" + style={{ display: "flex", justifyContent: "space-between" }} + > + <div> + <div class="subtitle">{url}</div> + <dl> + <dt> + <b>Error</b> + </dt> + <dd>{info.hint}</dd> + <dt> + <b>Code</b> + </dt> + <dd>{info.code}</dd> + <dt> + <b>Status</b> + </dt> + <dd>{status}</dd> + </dl> + </div> + <div + class="block" + style={{ + marginTop: "auto", + marginBottom: "auto", + display: "flex", + justifyContent: "space-between", + flexDirection: "column", + }} + > + <button class="button is-danger" onClick={() => onDelete(url)}> + Remove + </button> + </div> + </div> + ); +} diff --git a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx index f93ecfd8a..3018f88dd 100644 --- a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx @@ -17,7 +17,7 @@ import { AuthMethod, ReducerStateBackup } from "@gnu-taler/anastasis-core"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { useAnastasisContext } from "../../context/anastasis.js"; -import { AddingProviderScreen } from "./AddingProviderScreen.js"; +import AddingProviderScreen from "./AddingProviderScreen/index.js"; import { authMethods, AuthMethodSetupProps, @@ -84,7 +84,7 @@ export function AuthenticationEditorScreen(): VNode { if (manageProvider !== undefined) { return ( <AddingProviderScreen - onCancel={() => setManageProvider(undefined)} + onCancel={async () => setManageProvider(undefined)} providerType={ isKnownAuthMethods(manageProvider) ? manageProvider : undefined } diff --git a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx index 7d92bcd2c..11271aaa5 100644 --- a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx @@ -22,7 +22,7 @@ import { useEffect, useState } from "preact/hooks"; import { AsyncButton } from "../../components/AsyncButton.js"; import { PhoneNumberInput } from "../../components/fields/NumberInput.js"; import { useAnastasisContext } from "../../context/anastasis.js"; -import { AddingProviderScreen } from "./AddingProviderScreen.js"; +import AddingProviderScreen from "./AddingProviderScreen/index.js"; import { AnastasisClientFrame } from "./index.js"; export function SecretSelectionScreen(): VNode { @@ -54,7 +54,9 @@ export function SecretSelectionScreen(): VNode { const recoveryDocument = reducer.currentReducerState.recovery_document; if (manageProvider) { - return <AddingProviderScreen onCancel={() => setManageProvider(false)} />; + return ( + <AddingProviderScreen onCancel={async () => setManageProvider(false)} /> + ); } if (reducer.discoveryState.state === "none") { @@ -220,7 +222,9 @@ export function OldSecretSelectionScreen(): VNode { } if (manageProvider) { - return <AddingProviderScreen onCancel={() => setManageProvider(false)} />; + return ( + <AddingProviderScreen onCancel={async () => setManageProvider(false)} /> + ); } const providerInfo = provs[ diff --git a/packages/anastasis-webui/src/pages/home/index.storiesNo.tsx b/packages/anastasis-webui/src/pages/home/index.storiesNo.tsx index 1c47b57ee..0d01a5c89 100644 --- a/packages/anastasis-webui/src/pages/home/index.storiesNo.tsx +++ b/packages/anastasis-webui/src/pages/home/index.storiesNo.tsx @@ -19,7 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import * as a23 from "./AddingProviderScreen.stories.js"; +import * as a23 from "./AddingProviderScreen/stories.js"; import * as a28 from "./AttributeEntryScreen.stories.js"; import * as a18 from "./AuthenticationEditorScreen.stories.js"; import * as a8 from "./authMethod/AuthMethodEmailSetup.stories.js"; diff --git a/packages/anastasis-webui/src/pages/home/index.tsx b/packages/anastasis-webui/src/pages/home/index.tsx index 57f935bd6..25d9c63d9 100644 --- a/packages/anastasis-webui/src/pages/home/index.tsx +++ b/packages/anastasis-webui/src/pages/home/index.tsx @@ -168,9 +168,9 @@ export function AnastasisClientFrame(props: AnastasisClientFrameProps): VNode { window.removeEventListener("popstate", browserOnBackButton); }; }, []); - if (!reducer) { - return <p>Fatal: Reducer must be in context.</p>; - } + // if (!reducer) { + // return <p>Fatal: Reducer must be in context.</p>; + // } return ( <Fragment> diff --git a/packages/anastasis-webui/src/utils/index.tsx b/packages/anastasis-webui/src/utils/index.tsx index 204c48d18..63bed9392 100644 --- a/packages/anastasis-webui/src/utils/index.tsx +++ b/packages/anastasis-webui/src/utils/index.tsx @@ -21,13 +21,26 @@ import { ReducerState, ReducerStateRecovery, } from "@gnu-taler/anastasis-core"; -import { FunctionalComponent, h, VNode } from "preact"; +import { ComponentChildren, FunctionalComponent, h, VNode } from "preact"; import { AnastasisProvider } from "../context/anastasis.js"; const noop = async (): Promise<void> => { return; }; +export function createExampleWithoutAnastasis<Props>( + Component: FunctionalComponent<Props>, + props: Partial<Props> | (() => Partial<Props>), +): ComponentChildren { + //FIXME: props are evaluated on build time + // in some cases we want to evaluated the props on render time so we can get some relative timestamp + // check how we can build evaluatedProps in render time + const evaluatedProps = typeof props === "function" ? props() : props; + const Render = (args: any): VNode => h(Component, args); + Render.args = evaluatedProps; + return Render; +} + export function createExample<Props>( Component: FunctionalComponent<Props>, currentReducerState?: ReducerState, @@ -293,3 +306,24 @@ export const reducerStatesExample = { backup_state: BackupStates.TruthsPaying, } as ReducerState, }; + +export type StateFunc<S> = (p: S) => VNode; + +export type StateViewMap<StateType extends { status: string }> = { + [S in StateType as S["status"]]: StateFunc<S>; +}; + +export function compose<SType extends { status: string }, PType>( + name: string, + hook: (p: PType) => SType, + vs: StateViewMap<SType>, +): (p: PType) => VNode { + const Component = (p: PType): VNode => { + const state = hook(p); + const s = state.status as unknown as SType["status"]; + const c = vs[s] as unknown as StateFunc<SType>; + return c(state); + }; + Component.name = `${name}`; + return Component; +} |