elbe-ui 0.2.14 → 0.2.19
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.
- package/dist/bit/bit.d.ts +34 -0
- package/dist/bit/ctrl_bit.d.ts +30 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1383 -353
- package/dist/service/s_api.d.ts +30 -0
- package/dist/ui/components/box.d.ts +14 -14
- package/dist/ui/components/error_view.d.ts +15 -0
- package/dist/ui/components/spinner.d.ts +3 -0
- package/package.json +4 -2
- package/src/bit/bit.tsx +126 -0
- package/src/bit/ctrl_bit.tsx +112 -0
- package/src/index.tsx +4 -0
- package/src/service/s_api.ts +102 -0
- package/src/ui/components/error_view.tsx +72 -0
- package/src/ui/components/spinner.tsx +11 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface PostArgs {
|
|
2
|
+
path?: {
|
|
3
|
+
[key: string]: string | number | boolean | undefined;
|
|
4
|
+
};
|
|
5
|
+
query?: {
|
|
6
|
+
[key: string]: string | number | boolean | undefined;
|
|
7
|
+
};
|
|
8
|
+
body?: any;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* ApiService is a simple wrapper around fetch that handles JSON serialization and error handling.
|
|
12
|
+
* to use it, you must first call `ApiService.init(apiURL)` with the base URL of your API.
|
|
13
|
+
*/
|
|
14
|
+
export declare class ApiService {
|
|
15
|
+
private apiURL;
|
|
16
|
+
private static _i;
|
|
17
|
+
static get i(): ApiService;
|
|
18
|
+
private constructor();
|
|
19
|
+
static init(apiURL: string): void;
|
|
20
|
+
private _fetch;
|
|
21
|
+
get(path: string, args?: PostArgs): Promise<any>;
|
|
22
|
+
post(path: string, args: PostArgs): Promise<any>;
|
|
23
|
+
delete(path: string, args: PostArgs): Promise<any>;
|
|
24
|
+
}
|
|
25
|
+
export interface ApiError {
|
|
26
|
+
code: number;
|
|
27
|
+
message: string;
|
|
28
|
+
data?: any;
|
|
29
|
+
}
|
|
30
|
+
export declare function ifApiError(e: any): ApiError | null;
|
|
@@ -12,6 +12,13 @@ export declare function applyProps(p: ElbeProps, classes?: string | null | (stri
|
|
|
12
12
|
[x: string]: string | number | null | undefined;
|
|
13
13
|
[x: number]: string | number | null | undefined;
|
|
14
14
|
cssText?: string | null;
|
|
15
|
+
clipPath?: string | number | null | undefined;
|
|
16
|
+
filter?: string | number | null | undefined;
|
|
17
|
+
marker?: string | number | null | undefined;
|
|
18
|
+
mask?: string | number | null | undefined;
|
|
19
|
+
x?: string | number | null | undefined;
|
|
20
|
+
y?: string | number | null | undefined;
|
|
21
|
+
padding?: string | number | null | undefined;
|
|
15
22
|
accentColor?: string | number | null | undefined;
|
|
16
23
|
alignContent?: string | number | null | undefined;
|
|
17
24
|
alignItems?: string | number | null | undefined;
|
|
@@ -118,7 +125,6 @@ export declare function applyProps(p: ElbeProps, classes?: string | null | (stri
|
|
|
118
125
|
caretColor?: string | number | null | undefined;
|
|
119
126
|
clear?: string | number | null | undefined;
|
|
120
127
|
clip?: string | number | null | undefined;
|
|
121
|
-
clipPath?: string | number | null | undefined;
|
|
122
128
|
clipRule?: string | number | null | undefined;
|
|
123
129
|
color?: string | number | null | undefined;
|
|
124
130
|
colorInterpolation?: string | number | null | undefined;
|
|
@@ -160,7 +166,6 @@ export declare function applyProps(p: ElbeProps, classes?: string | null | (stri
|
|
|
160
166
|
fill?: string | number | null | undefined;
|
|
161
167
|
fillOpacity?: string | number | null | undefined;
|
|
162
168
|
fillRule?: string | number | null | undefined;
|
|
163
|
-
filter?: string | number | null | undefined;
|
|
164
169
|
flex?: string | number | null | undefined;
|
|
165
170
|
flexBasis?: string | number | null | undefined;
|
|
166
171
|
flexDirection?: string | number | null | undefined;
|
|
@@ -252,11 +257,9 @@ export declare function applyProps(p: ElbeProps, classes?: string | null | (stri
|
|
|
252
257
|
marginLeft?: string | number | null | undefined;
|
|
253
258
|
marginRight?: string | number | null | undefined;
|
|
254
259
|
marginTop?: string | number | null | undefined;
|
|
255
|
-
marker?: string | number | null | undefined;
|
|
256
260
|
markerEnd?: string | number | null | undefined;
|
|
257
261
|
markerMid?: string | number | null | undefined;
|
|
258
262
|
markerStart?: string | number | null | undefined;
|
|
259
|
-
mask?: string | number | null | undefined;
|
|
260
263
|
maskClip?: string | number | null | undefined;
|
|
261
264
|
maskComposite?: string | number | null | undefined;
|
|
262
265
|
maskImage?: string | number | null | undefined;
|
|
@@ -304,7 +307,6 @@ export declare function applyProps(p: ElbeProps, classes?: string | null | (stri
|
|
|
304
307
|
overscrollBehaviorInline?: string | number | null | undefined;
|
|
305
308
|
overscrollBehaviorX?: string | number | null | undefined;
|
|
306
309
|
overscrollBehaviorY?: string | number | null | undefined;
|
|
307
|
-
padding?: string | number | null | undefined;
|
|
308
310
|
paddingBlock?: string | number | null | undefined;
|
|
309
311
|
paddingBlockEnd?: string | number | null | undefined;
|
|
310
312
|
paddingBlockStart?: string | number | null | undefined;
|
|
@@ -506,8 +508,6 @@ export declare function applyProps(p: ElbeProps, classes?: string | null | (stri
|
|
|
506
508
|
wordSpacing?: string | number | null | undefined;
|
|
507
509
|
wordWrap?: string | number | null | undefined;
|
|
508
510
|
writingMode?: string | number | null | undefined;
|
|
509
|
-
x?: string | number | null | undefined;
|
|
510
|
-
y?: string | number | null | undefined;
|
|
511
511
|
zIndex?: string | number | null | undefined;
|
|
512
512
|
zoom?: string | number | null | undefined;
|
|
513
513
|
};
|
|
@@ -525,6 +525,13 @@ export declare function Box({ mode, scheme, padding, margin, children, ...elbe }
|
|
|
525
525
|
[x: string]: string | number | null | undefined;
|
|
526
526
|
[x: number]: string | number | null | undefined;
|
|
527
527
|
cssText?: string | null;
|
|
528
|
+
clipPath?: string | number | null | undefined;
|
|
529
|
+
filter?: string | number | null | undefined;
|
|
530
|
+
marker?: string | number | null | undefined;
|
|
531
|
+
mask?: string | number | null | undefined;
|
|
532
|
+
x?: string | number | null | undefined;
|
|
533
|
+
y?: string | number | null | undefined;
|
|
534
|
+
padding?: string | number | null | undefined;
|
|
528
535
|
accentColor?: string | number | null | undefined;
|
|
529
536
|
alignContent?: string | number | null | undefined;
|
|
530
537
|
alignItems?: string | number | null | undefined;
|
|
@@ -631,7 +638,6 @@ export declare function Box({ mode, scheme, padding, margin, children, ...elbe }
|
|
|
631
638
|
caretColor?: string | number | null | undefined;
|
|
632
639
|
clear?: string | number | null | undefined;
|
|
633
640
|
clip?: string | number | null | undefined;
|
|
634
|
-
clipPath?: string | number | null | undefined;
|
|
635
641
|
clipRule?: string | number | null | undefined;
|
|
636
642
|
color?: string | number | null | undefined;
|
|
637
643
|
colorInterpolation?: string | number | null | undefined;
|
|
@@ -673,7 +679,6 @@ export declare function Box({ mode, scheme, padding, margin, children, ...elbe }
|
|
|
673
679
|
fill?: string | number | null | undefined;
|
|
674
680
|
fillOpacity?: string | number | null | undefined;
|
|
675
681
|
fillRule?: string | number | null | undefined;
|
|
676
|
-
filter?: string | number | null | undefined;
|
|
677
682
|
flex?: string | number | null | undefined;
|
|
678
683
|
flexBasis?: string | number | null | undefined;
|
|
679
684
|
flexDirection?: string | number | null | undefined;
|
|
@@ -765,11 +770,9 @@ export declare function Box({ mode, scheme, padding, margin, children, ...elbe }
|
|
|
765
770
|
marginLeft?: string | number | null | undefined;
|
|
766
771
|
marginRight?: string | number | null | undefined;
|
|
767
772
|
marginTop?: string | number | null | undefined;
|
|
768
|
-
marker?: string | number | null | undefined;
|
|
769
773
|
markerEnd?: string | number | null | undefined;
|
|
770
774
|
markerMid?: string | number | null | undefined;
|
|
771
775
|
markerStart?: string | number | null | undefined;
|
|
772
|
-
mask?: string | number | null | undefined;
|
|
773
776
|
maskClip?: string | number | null | undefined;
|
|
774
777
|
maskComposite?: string | number | null | undefined;
|
|
775
778
|
maskImage?: string | number | null | undefined;
|
|
@@ -817,7 +820,6 @@ export declare function Box({ mode, scheme, padding, margin, children, ...elbe }
|
|
|
817
820
|
overscrollBehaviorInline?: string | number | null | undefined;
|
|
818
821
|
overscrollBehaviorX?: string | number | null | undefined;
|
|
819
822
|
overscrollBehaviorY?: string | number | null | undefined;
|
|
820
|
-
padding?: string | number | null | undefined;
|
|
821
823
|
paddingBlock?: string | number | null | undefined;
|
|
822
824
|
paddingBlockEnd?: string | number | null | undefined;
|
|
823
825
|
paddingBlockStart?: string | number | null | undefined;
|
|
@@ -1019,8 +1021,6 @@ export declare function Box({ mode, scheme, padding, margin, children, ...elbe }
|
|
|
1019
1021
|
wordSpacing?: string | number | null | undefined;
|
|
1020
1022
|
wordWrap?: string | number | null | undefined;
|
|
1021
1023
|
writingMode?: string | number | null | undefined;
|
|
1022
|
-
x?: string | number | null | undefined;
|
|
1023
|
-
y?: string | number | null | undefined;
|
|
1024
1024
|
zIndex?: string | number | null | undefined;
|
|
1025
1025
|
zoom?: string | number | null | undefined;
|
|
1026
1026
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ApiError } from "../../service/s_api";
|
|
2
|
+
export declare function ErrorView({ error, retry, debug, }: {
|
|
3
|
+
error: any;
|
|
4
|
+
retry?: () => any;
|
|
5
|
+
debug?: boolean;
|
|
6
|
+
}): import("preact").JSX.Element;
|
|
7
|
+
export declare function PrettyErrorView({ apiError, retry, labels, }: {
|
|
8
|
+
apiError: ApiError;
|
|
9
|
+
retry?: () => any;
|
|
10
|
+
labels?: {
|
|
11
|
+
retry?: string;
|
|
12
|
+
home?: string;
|
|
13
|
+
details?: string;
|
|
14
|
+
};
|
|
15
|
+
}): import("preact").JSX.Element;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "elbe-ui",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.19",
|
|
5
5
|
"author": "Robin Naumann",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -35,7 +35,9 @@
|
|
|
35
35
|
"typescript": "^5.0.0"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
+
"@preact/signals": "^1.3.0",
|
|
38
39
|
"lucide-react": "^0.438.0",
|
|
39
|
-
"preact": "^10.23.2"
|
|
40
|
+
"preact": "^10.23.2",
|
|
41
|
+
"preact-router": "^4.1.2"
|
|
40
42
|
}
|
|
41
43
|
}
|
package/src/bit/bit.tsx
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Signal, useSignal } from "@preact/signals";
|
|
2
|
+
import { type PreactContext, createContext } from "preact";
|
|
3
|
+
import { useContext } from "preact/hooks";
|
|
4
|
+
import { ErrorView } from "../ui/components/error_view";
|
|
5
|
+
import { Spinner } from "../ui/components/spinner";
|
|
6
|
+
|
|
7
|
+
export interface BitUseInterface<C, T> {
|
|
8
|
+
signal: Signal<BitState<T>>;
|
|
9
|
+
ctrl: C;
|
|
10
|
+
map: <D>(m: TriMap<T, D>) => D | preact.JSX.Element;
|
|
11
|
+
onData: (f: (d: T) => any) => any;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface BitData<C, T> {
|
|
15
|
+
ctrl: C;
|
|
16
|
+
state: Signal<BitState<T>>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface BitState<T> {
|
|
20
|
+
loading?: boolean;
|
|
21
|
+
error?: any;
|
|
22
|
+
data?: T;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type BitContext<T, C> = PreactContext<BitData<T, C> | null>;
|
|
26
|
+
|
|
27
|
+
export interface TriMap<T, D> {
|
|
28
|
+
onLoading?: () => D;
|
|
29
|
+
onError?: (e: string) => D;
|
|
30
|
+
onData?: (value: T) => D;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TWParams<T> {
|
|
34
|
+
emit: (t: T) => void;
|
|
35
|
+
emitLoading: () => void;
|
|
36
|
+
emitError: (e: any) => void;
|
|
37
|
+
map: <D>(m: TriMap<T, D>) => D;
|
|
38
|
+
signal: Signal<BitState<T>>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function makeBit<C, T>(name: string): BitContext<C, T> {
|
|
42
|
+
const c = createContext<BitData<C, T> | null>(null);
|
|
43
|
+
c.displayName = name;
|
|
44
|
+
return c;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function ProvideBit<I, C, T>(
|
|
48
|
+
context: BitContext<C, T>,
|
|
49
|
+
parameters: I,
|
|
50
|
+
worker: (p: I, d: TWParams<T>, ctrl: C) => void,
|
|
51
|
+
ctrl: (p: I, d: TWParams<T>) => C,
|
|
52
|
+
children: any
|
|
53
|
+
) {
|
|
54
|
+
const s = useSignal<BitState<T>>({ loading: true });
|
|
55
|
+
|
|
56
|
+
const _set = (n: BitState<T>) => {
|
|
57
|
+
if (JSON.stringify(n) === JSON.stringify(s.peek())) return;
|
|
58
|
+
s.value = n;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const emit = (data: T) => _set({ data });
|
|
62
|
+
const emitLoading = () => _set({ loading: true });
|
|
63
|
+
const emitError = (error: any) => {
|
|
64
|
+
console.warn(`BIT: ${context.displayName} emitted ERROR`, error);
|
|
65
|
+
return _set({ error });
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function map<D>(m: TriMap<T, D>) {
|
|
69
|
+
const st = s.value;
|
|
70
|
+
if (st.loading) return m.onLoading!();
|
|
71
|
+
if (st.error) return m.onError!(st.error);
|
|
72
|
+
return m.onData!(st.data ?? (null as any));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const c = ctrl(parameters, { emit, emitLoading, emitError, map, signal: s });
|
|
76
|
+
worker(parameters, { emit, emitLoading, emitError, map, signal: s }, c);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<context.Provider value={{ ctrl: c, state: s }}>
|
|
80
|
+
{children}
|
|
81
|
+
</context.Provider>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function useBit<C, T>(context: BitContext<C, T>): BitUseInterface<C, T> {
|
|
86
|
+
try {
|
|
87
|
+
const { ctrl, state } = useContext(context)!;
|
|
88
|
+
const v = state.value;
|
|
89
|
+
|
|
90
|
+
function map<D>(m: TriMap<T, D>) {
|
|
91
|
+
if (v.loading) return (m.onLoading || (() => <Spinner />))();
|
|
92
|
+
if (v.error)
|
|
93
|
+
return (
|
|
94
|
+
m.onError ||
|
|
95
|
+
((e) => <ErrorView error={e} retry={(ctrl as any).reload ?? null} />)
|
|
96
|
+
)(v.error);
|
|
97
|
+
return m.onData!(v.data ?? (null as any));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
signal: state,
|
|
102
|
+
ctrl,
|
|
103
|
+
map,
|
|
104
|
+
/**
|
|
105
|
+
* this is a quality of life function that allows
|
|
106
|
+
* you to chain the map function with the onData function
|
|
107
|
+
* @param f the builder function
|
|
108
|
+
* @returns the built component
|
|
109
|
+
*/
|
|
110
|
+
onData: (f: (d: T) => void) => map({ onData: f }),
|
|
111
|
+
};
|
|
112
|
+
} catch (e) {
|
|
113
|
+
const err = `BIT ERROR: NO ${context.displayName} PROVIDED`;
|
|
114
|
+
console.error(err, e);
|
|
115
|
+
return {
|
|
116
|
+
map: (_: any) => <div>{err}</div>,
|
|
117
|
+
ctrl: null as any,
|
|
118
|
+
signal: null as any,
|
|
119
|
+
onData: () => <div>{err}</div>,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function BitSpinner({ name }: { name: string }) {
|
|
125
|
+
return <Spinner />;
|
|
126
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useEffect } from "preact/hooks";
|
|
2
|
+
import type { JSX } from "preact/jsx-runtime";
|
|
3
|
+
import type { BitContext, BitUseInterface, TWParams } from "./bit";
|
|
4
|
+
import { makeBit as mb, ProvideBit, useBit } from "./bit";
|
|
5
|
+
|
|
6
|
+
abstract class BitControl<I, DT> {
|
|
7
|
+
p: I;
|
|
8
|
+
bit: TWParams<DT>;
|
|
9
|
+
|
|
10
|
+
constructor(p: I, bit: TWParams<DT>) {
|
|
11
|
+
this.bit = bit;
|
|
12
|
+
this.p = p;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
act(fn: (b: DT) => Promise<void>) {
|
|
16
|
+
this.bit.map({
|
|
17
|
+
onData: async (d) => {
|
|
18
|
+
try {
|
|
19
|
+
await fn(d);
|
|
20
|
+
} catch (e: any) {
|
|
21
|
+
if (e && e.code && e.message)
|
|
22
|
+
console.error(`[BitERROR] act: ${e.code} (${e.message})`);
|
|
23
|
+
else console.error("[BitERROR] act: ", e);
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Clean up resources. This is called once
|
|
31
|
+
* the element is removed from the DOM.
|
|
32
|
+
*/
|
|
33
|
+
dispose() {}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export abstract class WorkerControl<I, DT> extends BitControl<I, DT> {
|
|
37
|
+
reload: (() => Promise<void>) | null = null;
|
|
38
|
+
|
|
39
|
+
abstract worker(): Promise<DT>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export abstract class StreamControl<I, DT, Stream> extends BitControl<I, DT> {
|
|
43
|
+
protected stream: Stream | null = null;
|
|
44
|
+
abstract listen(): Stream;
|
|
45
|
+
|
|
46
|
+
dispose(): void {
|
|
47
|
+
if (this.stream) this.disposeStream(this.stream);
|
|
48
|
+
}
|
|
49
|
+
abstract disposeStream(stream: Stream): void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function make<I, DT, C extends BitControl<I, DT>>(
|
|
53
|
+
name: string
|
|
54
|
+
): BitContext<C, DT> {
|
|
55
|
+
return mb<C, DT>(name);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function use<I, DT, C extends BitControl<I, DT>>(
|
|
59
|
+
b: BitContext<C, DT>
|
|
60
|
+
): BitUseInterface<C, DT> {
|
|
61
|
+
return useBit<C, DT>(b);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function CtrlBit<I, DT, C extends BitControl<I, DT>>(
|
|
65
|
+
ctrl: (p: I, d: TWParams<DT>) => C,
|
|
66
|
+
name?: string
|
|
67
|
+
): {
|
|
68
|
+
Provide: (props: I & { children: React.ReactNode }) => JSX.Element;
|
|
69
|
+
use: () => BitUseInterface<C, DT>;
|
|
70
|
+
} {
|
|
71
|
+
const context = make<I, DT, C>((name || "Unknown") + "Bit");
|
|
72
|
+
|
|
73
|
+
function Provide({ children, ...p }: { children: React.ReactNode } & I) {
|
|
74
|
+
return ProvideBit(
|
|
75
|
+
context,
|
|
76
|
+
p,
|
|
77
|
+
async (p, b, c) => {
|
|
78
|
+
b.emitLoading();
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
if (c instanceof WorkerControl) {
|
|
82
|
+
if (c.reload) await c.reload();
|
|
83
|
+
}
|
|
84
|
+
if (c instanceof StreamControl) {
|
|
85
|
+
(c as any).stream = c.listen();
|
|
86
|
+
}
|
|
87
|
+
} catch (e) {
|
|
88
|
+
b.emitError(e);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
(p, b) => {
|
|
93
|
+
const c = ctrl(p as I, b);
|
|
94
|
+
// clean up on unmount
|
|
95
|
+
useEffect(() => () => c.dispose(), []);
|
|
96
|
+
if (c instanceof WorkerControl) {
|
|
97
|
+
c.reload = async () => {
|
|
98
|
+
b.emitLoading();
|
|
99
|
+
try {
|
|
100
|
+
b.emit(await c.worker());
|
|
101
|
+
} catch (e) {
|
|
102
|
+
b.emitError(e);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return c;
|
|
107
|
+
},
|
|
108
|
+
children
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return { Provide: Provide, use: () => use<I, DT, C>(context) };
|
|
112
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import * as Lucide from "lucide-react";
|
|
2
2
|
|
|
3
3
|
// exports
|
|
4
|
+
export * from "./bit/bit";
|
|
5
|
+
export * from "./bit/ctrl_bit";
|
|
6
|
+
export * from "./service/s_api";
|
|
4
7
|
export * from "./ui/color_theme";
|
|
5
8
|
export * from "./ui/components/badge";
|
|
6
9
|
export * from "./ui/components/box";
|
|
@@ -14,6 +17,7 @@ export * from "./ui/components/input/input_field";
|
|
|
14
17
|
export * from "./ui/components/input/range";
|
|
15
18
|
export * from "./ui/components/input/select";
|
|
16
19
|
export * from "./ui/components/padded";
|
|
20
|
+
export * from "./ui/components/spinner";
|
|
17
21
|
export * from "./ui/components/text";
|
|
18
22
|
export * from "./ui/components/toggle_button";
|
|
19
23
|
export * from "./ui/components/util";
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export interface PostArgs {
|
|
2
|
+
path?: { [key: string]: string | number | boolean | undefined };
|
|
3
|
+
query?: { [key: string]: string | number | boolean | undefined };
|
|
4
|
+
body?: any;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const _noArgs: PostArgs = {};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* ApiService is a simple wrapper around fetch that handles JSON serialization and error handling.
|
|
11
|
+
* to use it, you must first call `ApiService.init(apiURL)` with the base URL of your API.
|
|
12
|
+
*/
|
|
13
|
+
export class ApiService {
|
|
14
|
+
private static _i: ApiService | null = null;
|
|
15
|
+
public static get i(): ApiService {
|
|
16
|
+
if (!ApiService._i) throw "ApiService not initialized. Call ApiService.init(apiURL)";
|
|
17
|
+
return ApiService._i;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private constructor(private apiURL: string) {}
|
|
21
|
+
|
|
22
|
+
static init(apiURL: string) {
|
|
23
|
+
if (ApiService._i) throw "ApiService already initialized";
|
|
24
|
+
ApiService._i = new ApiService(apiURL);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private async _fetch(
|
|
28
|
+
p: string,
|
|
29
|
+
method: "GET" | "POST" | "DELETE",
|
|
30
|
+
{ path, query, body }: PostArgs
|
|
31
|
+
): Promise<any> {
|
|
32
|
+
try {
|
|
33
|
+
p = path
|
|
34
|
+
? p.replace(/:([a-zA-Z0-9_]+)/g, (m, p1) => {
|
|
35
|
+
const v = path[p1];
|
|
36
|
+
if (v === undefined)
|
|
37
|
+
throw { code: 400, message: `missing parameter ${p1}` };
|
|
38
|
+
return v?.toString() ?? "";
|
|
39
|
+
})
|
|
40
|
+
: p;
|
|
41
|
+
|
|
42
|
+
const queryStr =
|
|
43
|
+
query != null ? "?" + new URLSearchParams(query as any).toString() : "";
|
|
44
|
+
const response = await fetch(this.apiURL + p + queryStr, {
|
|
45
|
+
method,
|
|
46
|
+
credentials: "include",
|
|
47
|
+
headers: { "Content-Type": "application/json" },
|
|
48
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
49
|
+
});
|
|
50
|
+
if (response.ok) {
|
|
51
|
+
try {
|
|
52
|
+
return await response.json();
|
|
53
|
+
} catch (e) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
let data = null;
|
|
58
|
+
try {
|
|
59
|
+
data = await response.clone().json();
|
|
60
|
+
} catch (e) {
|
|
61
|
+
data = await response.text();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
throw {
|
|
65
|
+
code: response.status,
|
|
66
|
+
message: data.message ?? "undefined error",
|
|
67
|
+
data,
|
|
68
|
+
} as ApiError;
|
|
69
|
+
} catch (e) {
|
|
70
|
+
rethrow(e, 0, "unknown error");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async get(path: string, args?: PostArgs): Promise<any> {
|
|
75
|
+
return this._fetch(path, "GET", args || _noArgs);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async post(path: string, args: PostArgs): Promise<any> {
|
|
79
|
+
return this._fetch(path, "POST", args || _noArgs);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async delete(path: string, args: PostArgs): Promise<any> {
|
|
83
|
+
return this._fetch(path, "DELETE", args || _noArgs);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function rethrow(e: any, code: number, message: string): ApiError {
|
|
88
|
+
// if e implements the apiError interface, rethrow it:
|
|
89
|
+
if (e && e.code !== null && e.message !== null) throw e;
|
|
90
|
+
throw { code, message, data: e };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface ApiError {
|
|
94
|
+
code: number;
|
|
95
|
+
message: string;
|
|
96
|
+
data?: any;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function ifApiError(e: any): ApiError | null {
|
|
100
|
+
if (e && e.code !== null && e.message !== null) return e;
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useSignal } from "@preact/signals";
|
|
2
|
+
import { route } from "preact-router";
|
|
3
|
+
import { ElbeDialog, Icons } from "../..";
|
|
4
|
+
import { ifApiError, type ApiError } from "../../service/s_api";
|
|
5
|
+
|
|
6
|
+
export function ErrorView({
|
|
7
|
+
error,
|
|
8
|
+
retry,
|
|
9
|
+
debug,
|
|
10
|
+
}: {
|
|
11
|
+
error: any;
|
|
12
|
+
retry?: () => any;
|
|
13
|
+
debug?: boolean;
|
|
14
|
+
}) {
|
|
15
|
+
const apiError: ApiError = ifApiError(error) ?? {
|
|
16
|
+
code: 0,
|
|
17
|
+
message: "unknown error",
|
|
18
|
+
data: error,
|
|
19
|
+
};
|
|
20
|
+
return !debug ? (
|
|
21
|
+
<PrettyErrorView apiError={apiError} retry={retry} />
|
|
22
|
+
) : (
|
|
23
|
+
<div class="column padded card inverse cross-stretch">
|
|
24
|
+
<h3 style="margin: 0">ERROR: {apiError.code}</h3>
|
|
25
|
+
<p>{apiError.message}</p>
|
|
26
|
+
<pre>{JSON.stringify(apiError.data, null, 2)}</pre>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function PrettyErrorView({
|
|
32
|
+
apiError,
|
|
33
|
+
retry,
|
|
34
|
+
labels = {
|
|
35
|
+
retry: "retry",
|
|
36
|
+
home: "go home",
|
|
37
|
+
details: "error details",
|
|
38
|
+
},
|
|
39
|
+
}: {
|
|
40
|
+
apiError: ApiError;
|
|
41
|
+
retry?: () => any;
|
|
42
|
+
labels?: { retry?: string; home?: string; details?: string };
|
|
43
|
+
}) {
|
|
44
|
+
const openSig = useSignal(false);
|
|
45
|
+
return (
|
|
46
|
+
<div class="column padded cross-center" style="margin: 1rem 0">
|
|
47
|
+
<Icons.OctagonAlert />
|
|
48
|
+
<h3 style="margin: 0">{apiError.code}</h3>
|
|
49
|
+
<span class="pointer" onClick={() => (openSig.value = true)}>
|
|
50
|
+
{apiError.message}
|
|
51
|
+
</span>
|
|
52
|
+
{retry && (
|
|
53
|
+
<button class="action" onClick={() => retry()}>
|
|
54
|
+
<Icons.RotateCcw /> {labels.retry ?? "retry"}
|
|
55
|
+
</button>
|
|
56
|
+
)}
|
|
57
|
+
{apiError.code === 404 && (
|
|
58
|
+
<button class="action" onClick={() => route("/")}>
|
|
59
|
+
<Icons.House />
|
|
60
|
+
{labels.home ?? "go home"}
|
|
61
|
+
</button>
|
|
62
|
+
)}
|
|
63
|
+
<ElbeDialog
|
|
64
|
+
title={labels.details ?? "error details"}
|
|
65
|
+
open={openSig.value}
|
|
66
|
+
onClose={() => (openSig.value = false)}
|
|
67
|
+
>
|
|
68
|
+
<pre class="card inverse">{JSON.stringify(apiError.data, null, 2)}</pre>
|
|
69
|
+
</ElbeDialog>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|