react-cheminfo 0.58.0 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/lib/chart/ui/ChartTrackingLayer.js +3 -1
  2. package/lib/chart/ui/ChartTrackingLayer.js.map +1 -1
  3. package/lib/chart/ui/useChartSize.d.ts +16 -0
  4. package/lib/chart/ui/useChartSize.d.ts.map +1 -1
  5. package/lib/chart/ui/useChartSize.js +34 -1
  6. package/lib/chart/ui/useChartSize.js.map +1 -1
  7. package/lib/core.d.ts +1 -0
  8. package/lib/core.d.ts.map +1 -1
  9. package/lib/core.js +1 -0
  10. package/lib/core.js.map +1 -1
  11. package/lib/ecosystem/core/types.d.ts +1 -1
  12. package/lib/ecosystem/core/types.d.ts.map +1 -1
  13. package/lib/ecosystem/core/unlistedSites.d.ts.map +1 -1
  14. package/lib/ecosystem/core/unlistedSites.js +14 -0
  15. package/lib/ecosystem/core/unlistedSites.js.map +1 -1
  16. package/lib/ecosystem/ui/glyphs.d.ts.map +1 -1
  17. package/lib/ecosystem/ui/glyphs.js +3 -0
  18. package/lib/ecosystem/ui/glyphs.js.map +1 -1
  19. package/lib/i18n/core/chromeCatalog.d.ts +1 -1
  20. package/lib/i18n/core/chromeCatalog.d.ts.map +1 -1
  21. package/lib/locales/de.json +26 -1
  22. package/lib/locales/en.json +26 -1
  23. package/lib/locales/es.json +26 -1
  24. package/lib/locales/fr.json +26 -1
  25. package/lib/locales/it.json +26 -1
  26. package/lib/moleculesToken/core/index.d.ts +3 -0
  27. package/lib/moleculesToken/core/index.d.ts.map +1 -0
  28. package/lib/moleculesToken/core/index.js +2 -0
  29. package/lib/moleculesToken/core/index.js.map +1 -0
  30. package/lib/moleculesToken/core/moleculesToken.d.ts +101 -0
  31. package/lib/moleculesToken/core/moleculesToken.d.ts.map +1 -0
  32. package/lib/moleculesToken/core/moleculesToken.js +137 -0
  33. package/lib/moleculesToken/core/moleculesToken.js.map +1 -0
  34. package/lib/moleculesToken/ui/MoleculesTokenTable.d.ts +32 -0
  35. package/lib/moleculesToken/ui/MoleculesTokenTable.d.ts.map +1 -0
  36. package/lib/moleculesToken/ui/MoleculesTokenTable.js +124 -0
  37. package/lib/moleculesToken/ui/MoleculesTokenTable.js.map +1 -0
  38. package/lib/moleculesToken/ui/index.d.ts +3 -0
  39. package/lib/moleculesToken/ui/index.d.ts.map +1 -0
  40. package/lib/moleculesToken/ui/index.js +2 -0
  41. package/lib/moleculesToken/ui/index.js.map +1 -0
  42. package/lib/ui.d.ts +1 -0
  43. package/lib/ui.d.ts.map +1 -1
  44. package/lib/ui.js +1 -0
  45. package/lib/ui.js.map +1 -1
  46. package/package.json +1 -1
  47. package/src/chart/ui/ChartTrackingLayer.tsx +3 -1
  48. package/src/chart/ui/useChartSize.ts +44 -1
  49. package/src/core.ts +1 -0
  50. package/src/ecosystem/core/types.ts +2 -1
  51. package/src/ecosystem/core/unlistedSites.ts +14 -0
  52. package/src/ecosystem/ui/glyphs.tsx +10 -0
  53. package/src/locales/de.json +26 -1
  54. package/src/locales/en.json +26 -1
  55. package/src/locales/es.json +26 -1
  56. package/src/locales/fr.json +26 -1
  57. package/src/locales/it.json +26 -1
  58. package/src/moleculesToken/core/index.ts +11 -0
  59. package/src/moleculesToken/core/moleculesToken.ts +256 -0
  60. package/src/moleculesToken/ui/MoleculesTokenTable.tsx +249 -0
  61. package/src/moleculesToken/ui/index.ts +2 -0
  62. package/src/ui.ts +1 -0
  63. package/styles/chrome.css +35 -0
@@ -593,5 +593,30 @@
593
593
  "taxonomy.rank.noRank": "ohne Rang",
594
594
  "taxonomy.rank.clade": "Klade",
595
595
  "taxonomy.rank.cellularRoot": "zelluläre Wurzel",
596
- "taxonomy.rank.acellularRoot": "azelluläre Wurzel"
596
+ "taxonomy.rank.acellularRoot": "azelluläre Wurzel",
597
+ "moleculesToken.title": "Tokens für molecules.cheminfo.org",
598
+ "moleculesToken.checkedAt": "Geprüft am {when}",
599
+ "moleculesToken.checkAgain": "Erneut prüfen",
600
+ "moleculesToken.notChecked": "Noch nicht geprüft: Der Dienst, der in molecules.cheminfo.org schreibt, prüft seine Tokens beim Start.",
601
+ "moleculesToken.column.variable": "Variable",
602
+ "moleculesToken.column.source": "Quelle",
603
+ "moleculesToken.column.status": "Status",
604
+ "moleculesToken.column.details": "Details",
605
+ "moleculesToken.anySource": "beliebige Quelle",
606
+ "moleculesToken.verdict.ok": "gültig",
607
+ "moleculesToken.verdict.otherSource": "einer anderen Quelle",
608
+ "moleculesToken.verdict.refused": "abgelehnt",
609
+ "moleculesToken.verdict.unset": "nicht gesetzt",
610
+ "moleculesToken.verdict.uncheckable": "nicht prüfbar",
611
+ "moleculesToken.verdict.unanswered": "keine Antwort",
612
+ "moleculesToken.forSource": "für {source}",
613
+ "moleculesToken.issued": "ausgestellt am {when}",
614
+ "moleculesToken.issuedTo": "ausgestellt für {email} am {when}",
615
+ "moleculesToken.lastUsed": "zuletzt verwendet am {when}",
616
+ "moleculesToken.neverUsed": "nie verwendet",
617
+ "moleculesToken.otherSourceDetail": "ausgestellt für {source}: Was er sendet, wird dieser Quelle zugeschrieben",
618
+ "moleculesToken.refusedDetail": "nie ausgestellt oder widerrufen",
619
+ "moleculesToken.unsetDetail": "Setzen Sie {variable} in der .env des Servers.",
620
+ "moleculesToken.uncheckableDetail": "Dieses molecules.cheminfo.org kann noch keine Tokens prüfen.",
621
+ "moleculesToken.howToChange": "Ein Token wird in der .env des Servers geändert und beim Neustart des Dienstes neu gelesen. molecules.cheminfo.org stellt einen neuen aus mit"
597
622
  }
@@ -593,5 +593,30 @@
593
593
  "taxonomy.rank.noRank": "no rank",
594
594
  "taxonomy.rank.clade": "clade",
595
595
  "taxonomy.rank.cellularRoot": "cellular root",
596
- "taxonomy.rank.acellularRoot": "acellular root"
596
+ "taxonomy.rank.acellularRoot": "acellular root",
597
+ "moleculesToken.title": "Tokens for molecules.cheminfo.org",
598
+ "moleculesToken.checkedAt": "Checked {when}",
599
+ "moleculesToken.checkAgain": "Check again",
600
+ "moleculesToken.notChecked": "Not checked yet: the service that writes to molecules.cheminfo.org checks its tokens when it starts.",
601
+ "moleculesToken.column.variable": "Variable",
602
+ "moleculesToken.column.source": "Source",
603
+ "moleculesToken.column.status": "Status",
604
+ "moleculesToken.column.details": "Details",
605
+ "moleculesToken.anySource": "any source",
606
+ "moleculesToken.verdict.ok": "valid",
607
+ "moleculesToken.verdict.otherSource": "another source's",
608
+ "moleculesToken.verdict.refused": "refused",
609
+ "moleculesToken.verdict.unset": "not set",
610
+ "moleculesToken.verdict.uncheckable": "cannot be checked",
611
+ "moleculesToken.verdict.unanswered": "no answer",
612
+ "moleculesToken.forSource": "for {source}",
613
+ "moleculesToken.issued": "issued on {when}",
614
+ "moleculesToken.issuedTo": "issued to {email} on {when}",
615
+ "moleculesToken.lastUsed": "last used on {when}",
616
+ "moleculesToken.neverUsed": "never used",
617
+ "moleculesToken.otherSourceDetail": "issued for {source}: what it sends is credited to that source",
618
+ "moleculesToken.refusedDetail": "never issued, or revoked",
619
+ "moleculesToken.unsetDetail": "Set {variable} in the server's .env.",
620
+ "moleculesToken.uncheckableDetail": "This molecules.cheminfo.org cannot check a token yet.",
621
+ "moleculesToken.howToChange": "A token is changed in the server's .env and read again when the service restarts. molecules.cheminfo.org issues a new one with"
597
622
  }
@@ -593,5 +593,30 @@
593
593
  "taxonomy.rank.noRank": "sin rango",
594
594
  "taxonomy.rank.clade": "clado",
595
595
  "taxonomy.rank.cellularRoot": "raíz celular",
596
- "taxonomy.rank.acellularRoot": "raíz acelular"
596
+ "taxonomy.rank.acellularRoot": "raíz acelular",
597
+ "moleculesToken.title": "Tokens para molecules.cheminfo.org",
598
+ "moleculesToken.checkedAt": "Comprobados el {when}",
599
+ "moleculesToken.checkAgain": "Comprobar de nuevo",
600
+ "moleculesToken.notChecked": "Aún sin comprobar: el servicio que escribe en molecules.cheminfo.org comprueba sus tokens al arrancar.",
601
+ "moleculesToken.column.variable": "Variable",
602
+ "moleculesToken.column.source": "Fuente",
603
+ "moleculesToken.column.status": "Estado",
604
+ "moleculesToken.column.details": "Detalles",
605
+ "moleculesToken.anySource": "cualquier fuente",
606
+ "moleculesToken.verdict.ok": "válido",
607
+ "moleculesToken.verdict.otherSource": "de otra fuente",
608
+ "moleculesToken.verdict.refused": "rechazado",
609
+ "moleculesToken.verdict.unset": "sin definir",
610
+ "moleculesToken.verdict.uncheckable": "no comprobable",
611
+ "moleculesToken.verdict.unanswered": "sin respuesta",
612
+ "moleculesToken.forSource": "para {source}",
613
+ "moleculesToken.issued": "emitido el {when}",
614
+ "moleculesToken.issuedTo": "emitido para {email} el {when}",
615
+ "moleculesToken.lastUsed": "usado por última vez el {when}",
616
+ "moleculesToken.neverUsed": "nunca usado",
617
+ "moleculesToken.otherSourceDetail": "emitido para {source}: lo que envía se atribuye a esa fuente",
618
+ "moleculesToken.refusedDetail": "nunca emitido, o revocado",
619
+ "moleculesToken.unsetDetail": "Defina {variable} en el .env del servidor.",
620
+ "moleculesToken.uncheckableDetail": "Este molecules.cheminfo.org aún no puede comprobar un token.",
621
+ "moleculesToken.howToChange": "Un token se cambia en el .env del servidor y se vuelve a leer cuando el servicio se reinicia. molecules.cheminfo.org emite uno nuevo con"
597
622
  }
@@ -593,5 +593,30 @@
593
593
  "taxonomy.rank.noRank": "sans rang",
594
594
  "taxonomy.rank.clade": "clade",
595
595
  "taxonomy.rank.cellularRoot": "racine cellulaire",
596
- "taxonomy.rank.acellularRoot": "racine acellulaire"
596
+ "taxonomy.rank.acellularRoot": "racine acellulaire",
597
+ "moleculesToken.title": "Jetons pour molecules.cheminfo.org",
598
+ "moleculesToken.checkedAt": "Vérifiés le {when}",
599
+ "moleculesToken.checkAgain": "Vérifier à nouveau",
600
+ "moleculesToken.notChecked": "Pas encore vérifiés : le service qui écrit dans molecules.cheminfo.org vérifie ses jetons à son démarrage.",
601
+ "moleculesToken.column.variable": "Variable",
602
+ "moleculesToken.column.source": "Source",
603
+ "moleculesToken.column.status": "État",
604
+ "moleculesToken.column.details": "Détails",
605
+ "moleculesToken.anySource": "n'importe quelle source",
606
+ "moleculesToken.verdict.ok": "valide",
607
+ "moleculesToken.verdict.otherSource": "d'une autre source",
608
+ "moleculesToken.verdict.refused": "refusé",
609
+ "moleculesToken.verdict.unset": "non défini",
610
+ "moleculesToken.verdict.uncheckable": "invérifiable",
611
+ "moleculesToken.verdict.unanswered": "pas de réponse",
612
+ "moleculesToken.forSource": "pour {source}",
613
+ "moleculesToken.issued": "émis le {when}",
614
+ "moleculesToken.issuedTo": "émis pour {email} le {when}",
615
+ "moleculesToken.lastUsed": "dernière utilisation le {when}",
616
+ "moleculesToken.neverUsed": "jamais utilisé",
617
+ "moleculesToken.otherSourceDetail": "émis pour {source} : ce qu'il envoie est attribué à cette source",
618
+ "moleculesToken.refusedDetail": "jamais émis, ou révoqué",
619
+ "moleculesToken.unsetDetail": "Définissez {variable} dans le fichier .env du serveur.",
620
+ "moleculesToken.uncheckableDetail": "Ce molecules.cheminfo.org ne sait pas encore vérifier un jeton.",
621
+ "moleculesToken.howToChange": "Un jeton se change dans le fichier .env du serveur, et est relu au redémarrage du service. molecules.cheminfo.org en émet un nouveau avec"
597
622
  }
@@ -593,5 +593,30 @@
593
593
  "taxonomy.rank.noRank": "senza rango",
594
594
  "taxonomy.rank.clade": "clade",
595
595
  "taxonomy.rank.cellularRoot": "radice cellulare",
596
- "taxonomy.rank.acellularRoot": "radice acellulare"
596
+ "taxonomy.rank.acellularRoot": "radice acellulare",
597
+ "moleculesToken.title": "Token per molecules.cheminfo.org",
598
+ "moleculesToken.checkedAt": "Verificati il {when}",
599
+ "moleculesToken.checkAgain": "Verifica di nuovo",
600
+ "moleculesToken.notChecked": "Non ancora verificati: il servizio che scrive in molecules.cheminfo.org verifica i suoi token all'avvio.",
601
+ "moleculesToken.column.variable": "Variabile",
602
+ "moleculesToken.column.source": "Fonte",
603
+ "moleculesToken.column.status": "Stato",
604
+ "moleculesToken.column.details": "Dettagli",
605
+ "moleculesToken.anySource": "qualsiasi fonte",
606
+ "moleculesToken.verdict.ok": "valido",
607
+ "moleculesToken.verdict.otherSource": "di un'altra fonte",
608
+ "moleculesToken.verdict.refused": "rifiutato",
609
+ "moleculesToken.verdict.unset": "non impostato",
610
+ "moleculesToken.verdict.uncheckable": "non verificabile",
611
+ "moleculesToken.verdict.unanswered": "nessuna risposta",
612
+ "moleculesToken.forSource": "per {source}",
613
+ "moleculesToken.issued": "emesso il {when}",
614
+ "moleculesToken.issuedTo": "emesso per {email} il {when}",
615
+ "moleculesToken.lastUsed": "usato l'ultima volta il {when}",
616
+ "moleculesToken.neverUsed": "mai usato",
617
+ "moleculesToken.otherSourceDetail": "emesso per {source}: ciò che invia è attribuito a quella fonte",
618
+ "moleculesToken.refusedDetail": "mai emesso, o revocato",
619
+ "moleculesToken.unsetDetail": "Impostare {variable} nel file .env del server.",
620
+ "moleculesToken.uncheckableDetail": "Questo molecules.cheminfo.org non sa ancora verificare un token.",
621
+ "moleculesToken.howToChange": "Un token si cambia nel file .env del server e viene riletto al riavvio del servizio. molecules.cheminfo.org ne emette uno nuovo con"
597
622
  }
@@ -0,0 +1,11 @@
1
+ export type {
2
+ CheckMoleculesTokenOptions,
3
+ MoleculesTokenCheck,
4
+ MoleculesTokenToCheck,
5
+ MoleculesTokenVerdict,
6
+ } from './moleculesToken.ts';
7
+ export {
8
+ checkMoleculesToken,
9
+ checkMoleculesTokens,
10
+ publicTokenCheck,
11
+ } from './moleculesToken.ts';
@@ -0,0 +1,256 @@
1
+ import { trimTrailingSlash } from '../../router/core/address.ts';
2
+
3
+ /**
4
+ * What molecules.cheminfo.org says of a token a site holds:
5
+ *
6
+ * - `ok`: issued for a source the site writes, and not revoked;
7
+ * - `otherSource`: valid, but issued for another source, so what the site
8
+ * sends is credited to that one;
9
+ * - `refused`: never issued, or revoked;
10
+ * - `unset`: the site holds none;
11
+ * - `uncheckable`: molecules.cheminfo.org answers, but cannot check a token
12
+ * yet;
13
+ * - `unanswered`: it did not answer, or failed.
14
+ */
15
+ export type MoleculesTokenVerdict =
16
+ 'ok' | 'otherSource' | 'refused' | 'unset' | 'uncheckable' | 'unanswered';
17
+
18
+ /** A token a site holds, to be checked. */
19
+ export interface MoleculesTokenToCheck {
20
+ /** The environment variable it is read from, e.g. `MOLECULES_TOKEN_LOTUS`. */
21
+ variable: string;
22
+ /** The token, or undefined when the variable is unset or blank. */
23
+ token: string | undefined;
24
+ /**
25
+ * The sources it may have been issued for, as molecules.cheminfo.org names
26
+ * them: any one of them is right, whatever its case.
27
+ * @default [] — any source
28
+ */
29
+ sources?: readonly string[];
30
+ }
31
+
32
+ /** A token, as molecules.cheminfo.org checked it. Never the token itself. */
33
+ export interface MoleculesTokenCheck {
34
+ /** The environment variable it is read from. */
35
+ variable: string;
36
+ /** The sources it may have been issued for; empty for any source. */
37
+ sources: string[];
38
+ /** What molecules.cheminfo.org says of it. */
39
+ verdict: MoleculesTokenVerdict;
40
+ /**
41
+ * What molecules.cheminfo.org said, or why it could not be asked; null when
42
+ * the verdict says it all.
43
+ */
44
+ message: string | null;
45
+ /** The source molecules.cheminfo.org knows it for, once it answered. */
46
+ source: string | null;
47
+ /**
48
+ * Its first characters, as molecules.cheminfo.org shows them: what tells two
49
+ * tokens apart without showing either.
50
+ */
51
+ hint: string | null;
52
+ /** Who it was issued to. */
53
+ email: string | null;
54
+ /** When it was issued, in epoch milliseconds. */
55
+ issuedAt: number | null;
56
+ /** When it last read or wrote, in epoch milliseconds, to within a minute. */
57
+ lastUsedAt: number | null;
58
+ /** When it was checked, in epoch milliseconds. */
59
+ checkedAt: number;
60
+ }
61
+
62
+ /** Where molecules.cheminfo.org answers, and how patiently it is asked. */
63
+ export interface CheckMoleculesTokenOptions {
64
+ /** Where it answers, e.g. `https://molecules.cheminfo.org`. */
65
+ baseUrl: string;
66
+ /**
67
+ * How long one check may take.
68
+ * @default 5000
69
+ */
70
+ timeoutMs?: number;
71
+ /**
72
+ * The fetch to use, for tests.
73
+ * @default globalThis.fetch
74
+ */
75
+ fetch?: typeof fetch;
76
+ /**
77
+ * The clock the checks are stamped with, for tests.
78
+ * @default Date.now
79
+ */
80
+ now?: () => number;
81
+ }
82
+
83
+ /**
84
+ * Ask molecules.cheminfo.org about a token: `GET /v1/token`, which says which
85
+ * source it was issued for and never counts as using it. A token pasted in
86
+ * another source's place, revoked, or never issued shows here before an import
87
+ * sends anything with it. Never throws: a molecules.cheminfo.org that does not
88
+ * answer is a verdict too.
89
+ * @param entry - The token, where it is read from, and the sources it may be for.
90
+ * @param options - Where molecules.cheminfo.org answers, and how patiently.
91
+ * @returns What it says of the token.
92
+ */
93
+ export async function checkMoleculesToken(
94
+ entry: MoleculesTokenToCheck,
95
+ options: CheckMoleculesTokenOptions,
96
+ ): Promise<MoleculesTokenCheck> {
97
+ const {
98
+ baseUrl,
99
+ timeoutMs = 5000,
100
+ fetch: fetchImplementation = globalThis.fetch,
101
+ now = Date.now,
102
+ } = options;
103
+ const sources = entry.sources === undefined ? [] : [...entry.sources];
104
+ const verdict = (
105
+ value: MoleculesTokenVerdict,
106
+ message: string | null = null,
107
+ ): MoleculesTokenCheck => ({
108
+ variable: entry.variable,
109
+ sources,
110
+ verdict: value,
111
+ message,
112
+ source: null,
113
+ hint: null,
114
+ email: null,
115
+ issuedAt: null,
116
+ lastUsedAt: null,
117
+ checkedAt: now(),
118
+ });
119
+
120
+ const token = entry.token?.trim();
121
+ if (!token) return verdict('unset');
122
+
123
+ let response: Response;
124
+ try {
125
+ response = await fetchImplementation(
126
+ `${trimTrailingSlash(baseUrl)}/v1/token`,
127
+ {
128
+ headers: {
129
+ accept: 'application/json',
130
+ authorization: `Bearer ${token}`,
131
+ },
132
+ signal: AbortSignal.timeout(timeoutMs),
133
+ },
134
+ );
135
+ } catch (error: unknown) {
136
+ return verdict('unanswered', whyUnanswered(error, timeoutMs));
137
+ }
138
+
139
+ if (response.status === 401) {
140
+ return verdict('refused', await messageOf(response));
141
+ }
142
+ if (response.status === 404) {
143
+ await response.body?.cancel();
144
+ return verdict('uncheckable', 'GET /v1/token answered 404');
145
+ }
146
+ if (!response.ok) {
147
+ await response.body?.cancel();
148
+ return verdict('unanswered', `GET /v1/token answered ${response.status}`);
149
+ }
150
+
151
+ const record = await recordOf(response);
152
+ if (record === null) {
153
+ return verdict('unanswered', 'GET /v1/token answered no token');
154
+ }
155
+ const known = {
156
+ source: record.source,
157
+ hint: record.hint,
158
+ email: record.email,
159
+ issuedAt: record.createdAt * 1000,
160
+ lastUsedAt: record.lastUsedAt === null ? null : record.lastUsedAt * 1000,
161
+ };
162
+ if (record.revokedAt !== null) {
163
+ return { ...verdict('refused', 'this token was revoked'), ...known };
164
+ }
165
+ const wanted = record.source.toLowerCase();
166
+ const expected =
167
+ sources.length === 0 ||
168
+ sources.some((source) => source.toLowerCase() === wanted);
169
+ return { ...verdict(expected ? 'ok' : 'otherSource'), ...known };
170
+ }
171
+
172
+ /**
173
+ * Check every token a site holds, all at once.
174
+ * @param entries - The tokens. See {@link checkMoleculesToken}.
175
+ * @param options - See {@link CheckMoleculesTokenOptions}.
176
+ * @returns What molecules.cheminfo.org says of each, in the order given.
177
+ */
178
+ export async function checkMoleculesTokens(
179
+ entries: readonly MoleculesTokenToCheck[],
180
+ options: CheckMoleculesTokenOptions,
181
+ ): Promise<MoleculesTokenCheck[]> {
182
+ return Promise.all(
183
+ entries.map((entry) => checkMoleculesToken(entry, options)),
184
+ );
185
+ }
186
+
187
+ /**
188
+ * What a page anyone can open may show of a check: the verdict and the source,
189
+ * without who the token was issued to or its first characters.
190
+ * @param check - The check.
191
+ * @returns The check, without them.
192
+ */
193
+ export function publicTokenCheck(
194
+ check: MoleculesTokenCheck,
195
+ ): MoleculesTokenCheck {
196
+ return { ...check, email: null, hint: null };
197
+ }
198
+
199
+ /** What `GET /v1/token` answers, as far as a check reads it. */
200
+ interface TokenRecord {
201
+ source: string;
202
+ email: string;
203
+ hint: string;
204
+ createdAt: number;
205
+ lastUsedAt: number | null;
206
+ revokedAt: number | null;
207
+ }
208
+
209
+ async function recordOf(response: Response): Promise<TokenRecord | null> {
210
+ let body: unknown;
211
+ try {
212
+ body = await response.json();
213
+ } catch {
214
+ return null;
215
+ }
216
+ if (typeof body !== 'object' || body === null) return null;
217
+ const record = body as Partial<Record<keyof TokenRecord, unknown>>;
218
+ if (
219
+ typeof record.source !== 'string' ||
220
+ typeof record.email !== 'string' ||
221
+ typeof record.hint !== 'string' ||
222
+ typeof record.createdAt !== 'number' ||
223
+ !isOptionalNumber(record.lastUsedAt) ||
224
+ !isOptionalNumber(record.revokedAt)
225
+ ) {
226
+ return null;
227
+ }
228
+ return {
229
+ source: record.source,
230
+ email: record.email,
231
+ hint: record.hint,
232
+ createdAt: record.createdAt,
233
+ lastUsedAt: record.lastUsedAt,
234
+ revokedAt: record.revokedAt,
235
+ };
236
+ }
237
+
238
+ function isOptionalNumber(value: unknown): value is number | null {
239
+ return value === null || typeof value === 'number';
240
+ }
241
+
242
+ async function messageOf(response: Response): Promise<string | null> {
243
+ try {
244
+ const body = (await response.json()) as { message?: unknown };
245
+ return typeof body.message === 'string' ? body.message : null;
246
+ } catch {
247
+ return null;
248
+ }
249
+ }
250
+
251
+ function whyUnanswered(error: unknown, timeoutMs: number): string {
252
+ if ((error as { name?: unknown } | null)?.name === 'TimeoutError') {
253
+ return `no answer within ${timeoutMs} ms`;
254
+ }
255
+ return error instanceof Error ? error.message : String(error);
256
+ }
@@ -0,0 +1,249 @@
1
+ import type { Intent } from '@blueprintjs/core';
2
+ import { Button, Callout, HTMLTable, Tag } from '@blueprintjs/core';
3
+ import type { ReactNode } from 'react';
4
+ import { useMemo } from 'react';
5
+
6
+ import type { ChromeKey } from '../../i18n/core/chromeCatalog.ts';
7
+ import { useLanguage } from '../../i18n/ui/useLanguage.ts';
8
+ import type { Translate } from '../../i18n/ui/useT.ts';
9
+ import { useChromeT } from '../../i18n/ui/useT.ts';
10
+ import type {
11
+ MoleculesTokenCheck,
12
+ MoleculesTokenVerdict,
13
+ } from '../core/moleculesToken.ts';
14
+
15
+ /** What {@link MoleculesTokenTable} shows. */
16
+ export interface MoleculesTokenTableProps {
17
+ /** The last checks, or null before the first one was made. */
18
+ checks: readonly MoleculesTokenCheck[] | null;
19
+ /**
20
+ * Whether a check was asked for and is not made yet.
21
+ * @default false
22
+ */
23
+ pending?: boolean;
24
+ /**
25
+ * Asks for every token to be checked again. Left out, there is no button:
26
+ * a page anyone can open shows the last checks and asks for none.
27
+ * @default undefined
28
+ */
29
+ onCheckAgain?: () => void;
30
+ /**
31
+ * Why asking for a check failed.
32
+ * @default undefined
33
+ */
34
+ error?: string;
35
+ }
36
+
37
+ const INTENT: Record<MoleculesTokenVerdict, Intent> = {
38
+ ok: 'success',
39
+ otherSource: 'danger',
40
+ refused: 'danger',
41
+ unset: 'warning',
42
+ uncheckable: 'warning',
43
+ unanswered: 'warning',
44
+ };
45
+
46
+ /** How each verdict reads in the table. */
47
+ const LABEL: Record<MoleculesTokenVerdict, ChromeKey> = {
48
+ ok: 'moleculesToken.verdict.ok',
49
+ otherSource: 'moleculesToken.verdict.otherSource',
50
+ refused: 'moleculesToken.verdict.refused',
51
+ unset: 'moleculesToken.verdict.unset',
52
+ uncheckable: 'moleculesToken.verdict.uncheckable',
53
+ unanswered: 'moleculesToken.verdict.unanswered',
54
+ };
55
+
56
+ /** The command a token is issued with, on molecules.cheminfo.org. */
57
+ const ISSUE_COMMAND = 'npm run tokens -- issue SOURCE EMAIL';
58
+
59
+ /**
60
+ * The tokens a site holds for molecules.cheminfo.org, one row each, with what
61
+ * molecules.cheminfo.org said of it when it was last checked: valid, another
62
+ * source's, refused, not set. A token is changed in the server's `.env`, never
63
+ * here, so the table says where.
64
+ * @param props - See {@link MoleculesTokenTableProps}.
65
+ * @returns The table.
66
+ */
67
+ export function MoleculesTokenTable(props: MoleculesTokenTableProps) {
68
+ const { checks, pending = false, onCheckAgain, error } = props;
69
+ const t = useChromeT();
70
+ const language = useLanguage();
71
+ const formats = useMemo(
72
+ () => ({
73
+ instant: new Intl.DateTimeFormat(language, {
74
+ dateStyle: 'medium',
75
+ timeStyle: 'short',
76
+ }),
77
+ sources: new Intl.ListFormat(language, { type: 'disjunction' }),
78
+ }),
79
+ [language],
80
+ );
81
+ const checkedAt =
82
+ checks === null || checks.length === 0
83
+ ? null
84
+ : Math.min(...checks.map((check) => check.checkedAt));
85
+
86
+ return (
87
+ <section
88
+ className="molecules-tokens"
89
+ aria-label={t('moleculesToken.title')}
90
+ data-testid="molecules-tokens"
91
+ >
92
+ <div className="molecules-tokens__head">
93
+ <span className="molecules-tokens__title">
94
+ {t('moleculesToken.title')}
95
+ </span>
96
+ {checkedAt !== null && (
97
+ <span className="molecules-tokens__muted">
98
+ {t('moleculesToken.checkedAt', {
99
+ when: formats.instant.format(checkedAt),
100
+ })}
101
+ </span>
102
+ )}
103
+ {onCheckAgain !== undefined && (
104
+ <Button
105
+ size="small"
106
+ variant="minimal"
107
+ icon="refresh"
108
+ loading={pending}
109
+ text={t('moleculesToken.checkAgain')}
110
+ onClick={onCheckAgain}
111
+ data-testid="molecules-tokens-check"
112
+ />
113
+ )}
114
+ </div>
115
+ {error !== undefined && (
116
+ <Callout compact intent="danger">
117
+ {error}
118
+ </Callout>
119
+ )}
120
+ {checks === null ? (
121
+ <p className="molecules-tokens__muted">
122
+ {t('moleculesToken.notChecked')}
123
+ </p>
124
+ ) : (
125
+ <div className="molecules-tokens__scroll">
126
+ <HTMLTable compact striped className="molecules-tokens__table">
127
+ <thead>
128
+ <tr>
129
+ <th>{t('moleculesToken.column.variable')}</th>
130
+ <th>{t('moleculesToken.column.source')}</th>
131
+ <th>{t('moleculesToken.column.status')}</th>
132
+ <th>{t('moleculesToken.column.details')}</th>
133
+ </tr>
134
+ </thead>
135
+ <tbody>
136
+ {checks.map((check) => (
137
+ <tr key={check.variable} data-verdict={check.verdict}>
138
+ <td>
139
+ <code>{check.variable}</code>
140
+ </td>
141
+ <td>
142
+ {check.sources.length === 0
143
+ ? t('moleculesToken.anySource')
144
+ : formats.sources.format(check.sources)}
145
+ </td>
146
+ <td>
147
+ <Tag minimal intent={INTENT[check.verdict]}>
148
+ {t(LABEL[check.verdict])}
149
+ </Tag>
150
+ </td>
151
+ <td>{details(check, t, formats.instant)}</td>
152
+ </tr>
153
+ ))}
154
+ </tbody>
155
+ </HTMLTable>
156
+ </div>
157
+ )}
158
+ <p className="molecules-tokens__muted">
159
+ {t('moleculesToken.howToChange')} <code>{ISSUE_COMMAND}</code>
160
+ </p>
161
+ </section>
162
+ );
163
+ }
164
+
165
+ /**
166
+ * What a row says besides its verdict.
167
+ * @param check - The check.
168
+ * @param t - The chrome's translator.
169
+ * @param instant - How an instant is written.
170
+ * @returns The cell's content.
171
+ */
172
+ function details(
173
+ check: MoleculesTokenCheck,
174
+ t: Translate<ChromeKey>,
175
+ instant: Intl.DateTimeFormat,
176
+ ): ReactNode {
177
+ const hint: Fact | null =
178
+ check.hint === null
179
+ ? null
180
+ : ['hint', <code key="hint">{check.hint}…</code>];
181
+ switch (check.verdict) {
182
+ case 'ok':
183
+ return facts([
184
+ check.sources.length === 1 || check.source === null
185
+ ? null
186
+ : ['source', t('moleculesToken.forSource', { source: check.source })],
187
+ check.issuedAt === null
188
+ ? null
189
+ : [
190
+ 'issued',
191
+ check.email === null
192
+ ? t('moleculesToken.issued', {
193
+ when: instant.format(check.issuedAt),
194
+ })
195
+ : t('moleculesToken.issuedTo', {
196
+ email: check.email,
197
+ when: instant.format(check.issuedAt),
198
+ }),
199
+ ],
200
+ [
201
+ 'used',
202
+ check.lastUsedAt === null
203
+ ? t('moleculesToken.neverUsed')
204
+ : t('moleculesToken.lastUsed', {
205
+ when: instant.format(check.lastUsedAt),
206
+ }),
207
+ ],
208
+ hint,
209
+ ]);
210
+ case 'otherSource':
211
+ return facts([
212
+ [
213
+ 'source',
214
+ t('moleculesToken.otherSourceDetail', {
215
+ source: check.source ?? '',
216
+ }),
217
+ ],
218
+ hint,
219
+ ]);
220
+ case 'refused':
221
+ return check.message ?? t('moleculesToken.refusedDetail');
222
+ case 'unset':
223
+ return t('moleculesToken.unsetDetail', { variable: check.variable });
224
+ case 'uncheckable':
225
+ return t('moleculesToken.uncheckableDetail');
226
+ case 'unanswered':
227
+ return check.message;
228
+ default:
229
+ return null;
230
+ }
231
+ }
232
+
233
+ /** One fact about a token, and the key it is drawn under. */
234
+ type Fact = readonly [key: string, content: ReactNode];
235
+
236
+ /**
237
+ * Facts about a token, one after the other.
238
+ * @param parts - The facts; null ones are left out.
239
+ * @returns The facts, separated by a middle dot.
240
+ */
241
+ function facts(parts: ReadonlyArray<Fact | null>): ReactNode {
242
+ const shown = parts.filter((part) => part !== null);
243
+ return shown.map(([key, content], index) => (
244
+ <span key={key}>
245
+ {index > 0 && ' · '}
246
+ {content}
247
+ </span>
248
+ ));
249
+ }
@@ -0,0 +1,2 @@
1
+ export type { MoleculesTokenTableProps } from './MoleculesTokenTable.tsx';
2
+ export { MoleculesTokenTable } from './MoleculesTokenTable.tsx';