@everystack/admin 0.1.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.
- package/README.md +282 -0
- package/package.json +53 -0
- package/src/AdminRoot.tsx +34 -0
- package/src/adapters/EverystackAdapter.ts +218 -0
- package/src/adapters/types.ts +78 -0
- package/src/components/AvatarFieldInput.tsx +100 -0
- package/src/components/ImageDisplay.tsx +57 -0
- package/src/components/ImageFieldInput.tsx +140 -0
- package/src/components/dashboard/Brush.tsx +139 -0
- package/src/components/dashboard/DimensionBars.tsx +67 -0
- package/src/components/dashboard/DimensionDonut.tsx +98 -0
- package/src/components/dashboard/EventTable.tsx +248 -0
- package/src/components/dashboard/StatDials.tsx +67 -0
- package/src/components/dashboard/TimeHistogram.tsx +158 -0
- package/src/context.ts +75 -0
- package/src/hooks/useAdminConfig.ts +22 -0
- package/src/hooks/useAdminData.ts +132 -0
- package/src/hooks/useAggregates.ts +290 -0
- package/src/hooks/useRpc.ts +124 -0
- package/src/hooks/useStorageConfig.ts +16 -0
- package/src/index.ts +138 -0
- package/src/presets/twitter.ts +267 -0
- package/src/providers/AdminConfigProvider.tsx +18 -0
- package/src/providers/DataAdapterProvider.tsx +18 -0
- package/src/rpc/resource-dashboard-rpc.ts +416 -0
- package/src/screens/AdminDashboard.tsx +217 -0
- package/src/screens/AdminLayout.tsx +97 -0
- package/src/screens/ResourceCreate.tsx +243 -0
- package/src/screens/ResourceDashboard.tsx +278 -0
- package/src/screens/ResourceEdit.tsx +283 -0
- package/src/screens/ResourceList.tsx +153 -0
- package/src/screens/ResourceShow.tsx +171 -0
- package/src/screens/index.ts +14 -0
- package/src/types.ts +358 -0
- package/src/utils/naming.ts +105 -0
- package/src/utils/pick-image-web.ts +81 -0
- package/src/utils/resolve-image-url.ts +43 -0
- package/src/utils/upload-file.ts +93 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAggregates hook
|
|
3
|
+
*
|
|
4
|
+
* Provides aggregate query capabilities for dashboard charts and statistics.
|
|
5
|
+
* Uses the configured DataAdapter's aggregate method for PostgREST-style queries.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
9
|
+
import { useDataAdapter } from './useAdminData';
|
|
10
|
+
import type { AggregateParams, AggregateResult, AggregateQuery } from '../types';
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Types
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
export interface UseAggregatesOptions extends Omit<AggregateParams, 'aggregates'> {
|
|
17
|
+
/** Aggregate functions to compute */
|
|
18
|
+
aggregates: AggregateQuery[];
|
|
19
|
+
/** Skip initial fetch */
|
|
20
|
+
skip?: boolean;
|
|
21
|
+
/** Polling interval in milliseconds (0 = disabled) */
|
|
22
|
+
pollInterval?: number;
|
|
23
|
+
/** Callback when data is fetched successfully */
|
|
24
|
+
onSuccess?: (result: AggregateResult) => void;
|
|
25
|
+
/** Callback when fetch fails */
|
|
26
|
+
onError?: (error: Error) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface UseAggregatesResult {
|
|
30
|
+
/** Aggregated data rows */
|
|
31
|
+
data: Record<string, unknown>[];
|
|
32
|
+
/** Loading state */
|
|
33
|
+
loading: boolean;
|
|
34
|
+
/** Error state */
|
|
35
|
+
error: Error | null;
|
|
36
|
+
/** Refetch data manually */
|
|
37
|
+
refetch: () => Promise<void>;
|
|
38
|
+
/** Update filter and refetch */
|
|
39
|
+
setFilter: (filter: Record<string, unknown>) => void;
|
|
40
|
+
/** Whether the adapter supports aggregates */
|
|
41
|
+
isSupported: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// Hook Implementation
|
|
46
|
+
// =============================================================================
|
|
47
|
+
|
|
48
|
+
export function useAggregates(
|
|
49
|
+
resource: string,
|
|
50
|
+
options: UseAggregatesOptions
|
|
51
|
+
): UseAggregatesResult {
|
|
52
|
+
const adapter = useDataAdapter();
|
|
53
|
+
|
|
54
|
+
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
|
55
|
+
const [loading, setLoading] = useState(!options.skip);
|
|
56
|
+
const [error, setError] = useState<Error | null>(null);
|
|
57
|
+
const [filter, setFilterState] = useState(options.filter ?? {});
|
|
58
|
+
|
|
59
|
+
// Track if adapter supports aggregates
|
|
60
|
+
const isSupported = typeof adapter.aggregate === 'function';
|
|
61
|
+
|
|
62
|
+
// Use ref to track mounted state for cleanup
|
|
63
|
+
const isMounted = useRef(true);
|
|
64
|
+
|
|
65
|
+
// Stable reference for options
|
|
66
|
+
const aggregatesRef = useRef(options.aggregates);
|
|
67
|
+
const groupByRef = useRef(options.groupBy);
|
|
68
|
+
aggregatesRef.current = options.aggregates;
|
|
69
|
+
groupByRef.current = options.groupBy;
|
|
70
|
+
|
|
71
|
+
const fetchData = useCallback(async () => {
|
|
72
|
+
if (!isSupported) {
|
|
73
|
+
setError(new Error('DataAdapter does not support aggregate queries'));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
setLoading(true);
|
|
78
|
+
setError(null);
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const result = await adapter.aggregate!(resource, {
|
|
82
|
+
aggregates: aggregatesRef.current,
|
|
83
|
+
groupBy: groupByRef.current,
|
|
84
|
+
filter,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (isMounted.current) {
|
|
88
|
+
setData(result.data);
|
|
89
|
+
options.onSuccess?.(result);
|
|
90
|
+
}
|
|
91
|
+
} catch (err) {
|
|
92
|
+
if (isMounted.current) {
|
|
93
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
94
|
+
setError(error);
|
|
95
|
+
options.onError?.(error);
|
|
96
|
+
}
|
|
97
|
+
} finally {
|
|
98
|
+
if (isMounted.current) {
|
|
99
|
+
setLoading(false);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}, [adapter, resource, filter, isSupported, options.onSuccess, options.onError]);
|
|
103
|
+
|
|
104
|
+
// Set error if adapter doesn't support aggregates
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (!isSupported && !options.skip) {
|
|
107
|
+
setError(new Error('DataAdapter does not support aggregate queries'));
|
|
108
|
+
setLoading(false);
|
|
109
|
+
}
|
|
110
|
+
}, [isSupported, options.skip]);
|
|
111
|
+
|
|
112
|
+
// Initial fetch
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (!options.skip && isSupported) {
|
|
115
|
+
fetchData();
|
|
116
|
+
}
|
|
117
|
+
}, [fetchData, options.skip, isSupported]);
|
|
118
|
+
|
|
119
|
+
// Polling
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (options.pollInterval && options.pollInterval > 0 && isSupported) {
|
|
122
|
+
const intervalId = setInterval(fetchData, options.pollInterval);
|
|
123
|
+
return () => clearInterval(intervalId);
|
|
124
|
+
}
|
|
125
|
+
}, [fetchData, options.pollInterval, isSupported]);
|
|
126
|
+
|
|
127
|
+
// Cleanup on unmount
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
isMounted.current = true;
|
|
130
|
+
return () => {
|
|
131
|
+
isMounted.current = false;
|
|
132
|
+
};
|
|
133
|
+
}, []);
|
|
134
|
+
|
|
135
|
+
const setFilter = useCallback((newFilter: Record<string, unknown>) => {
|
|
136
|
+
setFilterState(newFilter);
|
|
137
|
+
}, []);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
data,
|
|
141
|
+
loading,
|
|
142
|
+
error,
|
|
143
|
+
refetch: fetchData,
|
|
144
|
+
setFilter,
|
|
145
|
+
isSupported,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// =============================================================================
|
|
150
|
+
// Convenience Hooks
|
|
151
|
+
// =============================================================================
|
|
152
|
+
|
|
153
|
+
export interface UseCountOptions {
|
|
154
|
+
column?: string;
|
|
155
|
+
distinct?: boolean;
|
|
156
|
+
filter?: Record<string, unknown>;
|
|
157
|
+
skip?: boolean;
|
|
158
|
+
pollInterval?: number;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface UseCountResult {
|
|
162
|
+
count: number | null;
|
|
163
|
+
loading: boolean;
|
|
164
|
+
error: Error | null;
|
|
165
|
+
refetch: () => Promise<void>;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function useCount(resource: string, options: UseCountOptions = {}): UseCountResult {
|
|
169
|
+
const result = useAggregates(resource, {
|
|
170
|
+
aggregates: [
|
|
171
|
+
{
|
|
172
|
+
function: 'count',
|
|
173
|
+
column: options.column,
|
|
174
|
+
distinct: options.distinct,
|
|
175
|
+
alias: 'count',
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
filter: options.filter,
|
|
179
|
+
skip: options.skip,
|
|
180
|
+
pollInterval: options.pollInterval,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const count = (result.data[0]?.count as number | null) ?? null;
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
count,
|
|
187
|
+
loading: result.loading,
|
|
188
|
+
error: result.error,
|
|
189
|
+
refetch: result.refetch,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface UseGroupedCountOptions {
|
|
194
|
+
filter?: Record<string, unknown>;
|
|
195
|
+
skip?: boolean;
|
|
196
|
+
pollInterval?: number;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface UseGroupedCountResult {
|
|
200
|
+
data: { [key: string]: unknown; count: number }[];
|
|
201
|
+
loading: boolean;
|
|
202
|
+
error: Error | null;
|
|
203
|
+
refetch: () => Promise<void>;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function useGroupedCount(
|
|
207
|
+
resource: string,
|
|
208
|
+
groupBy: string,
|
|
209
|
+
options: UseGroupedCountOptions = {}
|
|
210
|
+
): UseGroupedCountResult {
|
|
211
|
+
const result = useAggregates(resource, {
|
|
212
|
+
aggregates: [{ function: 'count', alias: 'count' }],
|
|
213
|
+
groupBy: [groupBy],
|
|
214
|
+
filter: options.filter,
|
|
215
|
+
skip: options.skip,
|
|
216
|
+
pollInterval: options.pollInterval,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
data: result.data as { [key: string]: unknown; count: number }[],
|
|
221
|
+
loading: result.loading,
|
|
222
|
+
error: result.error,
|
|
223
|
+
refetch: result.refetch,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface UseStatsOptions {
|
|
228
|
+
count?: boolean;
|
|
229
|
+
sum?: string;
|
|
230
|
+
avg?: string;
|
|
231
|
+
min?: string;
|
|
232
|
+
max?: string;
|
|
233
|
+
filter?: Record<string, unknown>;
|
|
234
|
+
skip?: boolean;
|
|
235
|
+
pollInterval?: number;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export interface UseStatsResult {
|
|
239
|
+
stats: {
|
|
240
|
+
count?: number;
|
|
241
|
+
sum?: number;
|
|
242
|
+
avg?: number;
|
|
243
|
+
min?: number;
|
|
244
|
+
max?: number;
|
|
245
|
+
};
|
|
246
|
+
loading: boolean;
|
|
247
|
+
error: Error | null;
|
|
248
|
+
refetch: () => Promise<void>;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function useStats(resource: string, options: UseStatsOptions = {}): UseStatsResult {
|
|
252
|
+
const aggregates: AggregateQuery[] = [];
|
|
253
|
+
|
|
254
|
+
if (options.count) {
|
|
255
|
+
aggregates.push({ function: 'count', alias: 'count' });
|
|
256
|
+
}
|
|
257
|
+
if (options.sum) {
|
|
258
|
+
aggregates.push({ function: 'sum', column: options.sum, alias: 'sum' });
|
|
259
|
+
}
|
|
260
|
+
if (options.avg) {
|
|
261
|
+
aggregates.push({ function: 'avg', column: options.avg, alias: 'avg' });
|
|
262
|
+
}
|
|
263
|
+
if (options.min) {
|
|
264
|
+
aggregates.push({ function: 'min', column: options.min, alias: 'min' });
|
|
265
|
+
}
|
|
266
|
+
if (options.max) {
|
|
267
|
+
aggregates.push({ function: 'max', column: options.max, alias: 'max' });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Default to count if no options specified
|
|
271
|
+
if (aggregates.length === 0) {
|
|
272
|
+
aggregates.push({ function: 'count', alias: 'count' });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const result = useAggregates(resource, {
|
|
276
|
+
aggregates,
|
|
277
|
+
filter: options.filter,
|
|
278
|
+
skip: options.skip,
|
|
279
|
+
pollInterval: options.pollInterval,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const stats = (result.data[0] ?? {}) as UseStatsResult['stats'];
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
stats,
|
|
286
|
+
loading: result.loading,
|
|
287
|
+
error: result.error,
|
|
288
|
+
refetch: result.refetch,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useRpc — Lightweight hook for calling RPC endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Calls POST {baseUrl}/rpc/{fn} with JSON body.
|
|
5
|
+
* Manages loading/error state and supports manual refetch.
|
|
6
|
+
*
|
|
7
|
+
* Resilience features:
|
|
8
|
+
* - Debounces rapid body changes (150ms) to prevent request storms
|
|
9
|
+
* - AbortController cancels stale in-flight requests
|
|
10
|
+
* - Keeps previous data visible during refetch (no flash to loading state)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
14
|
+
|
|
15
|
+
export interface UseRpcOptions {
|
|
16
|
+
baseUrl: string;
|
|
17
|
+
fn: string;
|
|
18
|
+
body?: unknown;
|
|
19
|
+
skip?: boolean;
|
|
20
|
+
/** Debounce delay in ms before firing request on body change. Default: 150 */
|
|
21
|
+
debounce?: number;
|
|
22
|
+
getToken?: () => string | null | Promise<string | null>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface UseRpcResult<T> {
|
|
26
|
+
data: T | null;
|
|
27
|
+
loading: boolean;
|
|
28
|
+
error: Error | null;
|
|
29
|
+
refetch: () => Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function useRpc<T = unknown>(options: UseRpcOptions): UseRpcResult<T> {
|
|
33
|
+
const { baseUrl, fn, body, skip, debounce = 150, getToken } = options;
|
|
34
|
+
const [data, setData] = useState<T | null>(null);
|
|
35
|
+
const [loading, setLoading] = useState(!skip);
|
|
36
|
+
const [error, setError] = useState<Error | null>(null);
|
|
37
|
+
|
|
38
|
+
const serializedBody = JSON.stringify(body) ?? '{}';
|
|
39
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
40
|
+
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
41
|
+
|
|
42
|
+
const fetchData = useCallback(async (signal?: AbortSignal) => {
|
|
43
|
+
setLoading(true);
|
|
44
|
+
setError(null);
|
|
45
|
+
try {
|
|
46
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
47
|
+
if (getToken) {
|
|
48
|
+
const token = await getToken();
|
|
49
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
50
|
+
}
|
|
51
|
+
const res = await fetch(`${baseUrl}/rpc/${fn}`, {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers,
|
|
54
|
+
body: serializedBody,
|
|
55
|
+
signal,
|
|
56
|
+
});
|
|
57
|
+
if (signal?.aborted) return;
|
|
58
|
+
if (!res.ok) {
|
|
59
|
+
throw new Error(`RPC ${fn} failed: ${res.status}`);
|
|
60
|
+
}
|
|
61
|
+
const json = await res.json();
|
|
62
|
+
if (!signal?.aborted) {
|
|
63
|
+
setData(json as T);
|
|
64
|
+
}
|
|
65
|
+
} catch (err) {
|
|
66
|
+
if (err instanceof DOMException && err.name === 'AbortError') return;
|
|
67
|
+
if (!signal?.aborted) {
|
|
68
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
69
|
+
}
|
|
70
|
+
} finally {
|
|
71
|
+
if (!signal?.aborted) {
|
|
72
|
+
setLoading(false);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
76
|
+
}, [baseUrl, fn, getToken, serializedBody]);
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (skip) return;
|
|
80
|
+
|
|
81
|
+
// Clear any pending debounce
|
|
82
|
+
if (debounceRef.current) {
|
|
83
|
+
clearTimeout(debounceRef.current);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Abort previous in-flight request
|
|
87
|
+
if (abortRef.current) {
|
|
88
|
+
abortRef.current.abort();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Debounce the fetch (skip debounce for initial load when no data yet)
|
|
92
|
+
const delay = data === null ? 0 : debounce;
|
|
93
|
+
debounceRef.current = setTimeout(() => {
|
|
94
|
+
const controller = new AbortController();
|
|
95
|
+
abortRef.current = controller;
|
|
96
|
+
fetchData(controller.signal);
|
|
97
|
+
}, delay);
|
|
98
|
+
|
|
99
|
+
return () => {
|
|
100
|
+
if (debounceRef.current) {
|
|
101
|
+
clearTimeout(debounceRef.current);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
105
|
+
}, [fetchData, skip]);
|
|
106
|
+
|
|
107
|
+
// Manual refetch (bypasses debounce)
|
|
108
|
+
const refetch = useCallback(async () => {
|
|
109
|
+
if (abortRef.current) abortRef.current.abort();
|
|
110
|
+
const controller = new AbortController();
|
|
111
|
+
abortRef.current = controller;
|
|
112
|
+
await fetchData(controller.signal);
|
|
113
|
+
}, [fetchData]);
|
|
114
|
+
|
|
115
|
+
// Cleanup on unmount
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
return () => {
|
|
118
|
+
if (abortRef.current) abortRef.current.abort();
|
|
119
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
120
|
+
};
|
|
121
|
+
}, []);
|
|
122
|
+
|
|
123
|
+
return { data, loading, error, refetch };
|
|
124
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useStorageConfig
|
|
3
|
+
*
|
|
4
|
+
* Provides storage configuration (endpoint, CDN URL, auth) to admin
|
|
5
|
+
* image/file field components via React context. Nullable by design —
|
|
6
|
+
* when storage is not configured, image fields degrade to plain TextInput.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createContext, useContext } from 'react';
|
|
10
|
+
import type { StorageConfig } from '../types';
|
|
11
|
+
|
|
12
|
+
export const StorageConfigContext = createContext<StorageConfig | null>(null);
|
|
13
|
+
|
|
14
|
+
export function useStorageConfig(): StorageConfig | null {
|
|
15
|
+
return useContext(StorageConfigContext);
|
|
16
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* everystack/admin — Declarative admin dashboard framework.
|
|
3
|
+
*
|
|
4
|
+
* Provides a configuration-driven admin UI with hooks for data access,
|
|
5
|
+
* an EverystackAdapter that bridges to everystack/client, and utilities
|
|
6
|
+
* for building admin dashboards.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Types
|
|
10
|
+
export type {
|
|
11
|
+
FieldType,
|
|
12
|
+
FieldDefinition,
|
|
13
|
+
ResourceConfig,
|
|
14
|
+
StorageConfig,
|
|
15
|
+
WidgetType,
|
|
16
|
+
ChartType,
|
|
17
|
+
WidgetConfig,
|
|
18
|
+
DashboardConfig,
|
|
19
|
+
MenuItem,
|
|
20
|
+
AdminConfig,
|
|
21
|
+
ListParams,
|
|
22
|
+
ListResult,
|
|
23
|
+
DataRecord,
|
|
24
|
+
AggregateFunction,
|
|
25
|
+
AggregateQuery,
|
|
26
|
+
AggregateParams,
|
|
27
|
+
AggregateResult,
|
|
28
|
+
// Resource Dashboard types
|
|
29
|
+
ResourceDashboardConfig,
|
|
30
|
+
StatDialConfig,
|
|
31
|
+
StatDialValue,
|
|
32
|
+
DimensionConfig,
|
|
33
|
+
} from './types';
|
|
34
|
+
|
|
35
|
+
// Adapter types
|
|
36
|
+
export type {
|
|
37
|
+
DataAdapter,
|
|
38
|
+
SubscriptionEventType,
|
|
39
|
+
SubscriptionEvent,
|
|
40
|
+
} from './adapters/types';
|
|
41
|
+
|
|
42
|
+
// EverystackAdapter
|
|
43
|
+
export { EverystackAdapter, type EverystackAdapterOptions } from './adapters/EverystackAdapter';
|
|
44
|
+
|
|
45
|
+
// Hooks
|
|
46
|
+
export { useAdminConfig, type AdminConfigContextValue } from './hooks/useAdminConfig';
|
|
47
|
+
export {
|
|
48
|
+
useDataAdapter,
|
|
49
|
+
useAdminData,
|
|
50
|
+
type UseAdminDataOptions,
|
|
51
|
+
type UseAdminDataResult,
|
|
52
|
+
} from './hooks/useAdminData';
|
|
53
|
+
export {
|
|
54
|
+
useAggregates,
|
|
55
|
+
useCount,
|
|
56
|
+
useGroupedCount,
|
|
57
|
+
useStats,
|
|
58
|
+
type UseAggregatesOptions,
|
|
59
|
+
type UseAggregatesResult,
|
|
60
|
+
type UseCountOptions,
|
|
61
|
+
type UseCountResult,
|
|
62
|
+
type UseGroupedCountOptions,
|
|
63
|
+
type UseGroupedCountResult,
|
|
64
|
+
type UseStatsOptions,
|
|
65
|
+
type UseStatsResult,
|
|
66
|
+
} from './hooks/useAggregates';
|
|
67
|
+
|
|
68
|
+
// useRpc hook
|
|
69
|
+
export { useRpc, type UseRpcOptions, type UseRpcResult } from './hooks/useRpc';
|
|
70
|
+
|
|
71
|
+
// RPC Factory
|
|
72
|
+
export {
|
|
73
|
+
createResourceDashboardRpcs,
|
|
74
|
+
createAllDashboardRpcs,
|
|
75
|
+
type ResourceDashboardRpcs,
|
|
76
|
+
type DashboardFilters,
|
|
77
|
+
type DashboardAggregates,
|
|
78
|
+
type EventListResult,
|
|
79
|
+
} from './rpc/resource-dashboard-rpc';
|
|
80
|
+
|
|
81
|
+
// Dashboard Components
|
|
82
|
+
export { StatDials, type DialData, type StatDialsProps } from './components/dashboard/StatDials';
|
|
83
|
+
export { DimensionBars, type DimensionBarsProps } from './components/dashboard/DimensionBars';
|
|
84
|
+
export { DimensionDonut, type DimensionDonutProps } from './components/dashboard/DimensionDonut';
|
|
85
|
+
export { EventTable, type EventTableProps, type EventTableColumn } from './components/dashboard/EventTable';
|
|
86
|
+
export { TimeHistogram, type TimeHistogramProps } from './components/dashboard/TimeHistogram';
|
|
87
|
+
export { Brush, type BrushProps, clamp, computeBrushExtent } from './components/dashboard/Brush';
|
|
88
|
+
|
|
89
|
+
// Providers
|
|
90
|
+
export { AdminConfigProvider, type AdminConfigProviderProps } from './providers/AdminConfigProvider';
|
|
91
|
+
export { DataAdapterProvider, type DataAdapterProviderProps } from './providers/DataAdapterProvider';
|
|
92
|
+
|
|
93
|
+
// AdminRoot
|
|
94
|
+
export { AdminRoot, type AdminRootProps } from './AdminRoot';
|
|
95
|
+
|
|
96
|
+
// Screens
|
|
97
|
+
export {
|
|
98
|
+
AdminLayout,
|
|
99
|
+
AdminDashboard,
|
|
100
|
+
ResourceList,
|
|
101
|
+
ResourceShow,
|
|
102
|
+
ResourceCreate,
|
|
103
|
+
ResourceEdit,
|
|
104
|
+
ResourceDashboard,
|
|
105
|
+
type AdminLayoutProps,
|
|
106
|
+
type AdminDashboardProps,
|
|
107
|
+
type ResourceListProps,
|
|
108
|
+
type ResourceShowProps,
|
|
109
|
+
type ResourceCreateProps,
|
|
110
|
+
type ResourceEditProps,
|
|
111
|
+
type ResourceDashboardProps,
|
|
112
|
+
} from './screens';
|
|
113
|
+
|
|
114
|
+
// Context (virtual routing for expo-router)
|
|
115
|
+
export { createAdminContext, type RequireContext } from './context';
|
|
116
|
+
|
|
117
|
+
// Storage hooks
|
|
118
|
+
export { useStorageConfig, StorageConfigContext } from './hooks/useStorageConfig';
|
|
119
|
+
|
|
120
|
+
// Image field components
|
|
121
|
+
export { ImageFieldInput } from './components/ImageFieldInput';
|
|
122
|
+
export { AvatarFieldInput } from './components/AvatarFieldInput';
|
|
123
|
+
export { ImageDisplay } from './components/ImageDisplay';
|
|
124
|
+
|
|
125
|
+
// Image utilities
|
|
126
|
+
export { resolveImageUrl, type ResolveImageUrlOptions } from './utils/resolve-image-url';
|
|
127
|
+
export { uploadFile, type UploadFileOptions, type UploadFileResult } from './utils/upload-file';
|
|
128
|
+
|
|
129
|
+
// Utilities
|
|
130
|
+
export {
|
|
131
|
+
toCamelCase,
|
|
132
|
+
toSnakeCase,
|
|
133
|
+
getFieldValue,
|
|
134
|
+
hasField,
|
|
135
|
+
resolveFieldKey,
|
|
136
|
+
toSnakeCaseKeys,
|
|
137
|
+
toCamelCaseKeys,
|
|
138
|
+
} from './utils/naming';
|