@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,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Twitter Admin Preset
|
|
3
|
+
*
|
|
4
|
+
* Pre-built admin configuration for the example Twitter clone app.
|
|
5
|
+
* Covers profiles, posts, likes, and follows resources matching
|
|
6
|
+
* the example app's Drizzle schema.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AdminConfig, ResourceConfig, ResourceDashboardConfig, DashboardConfig } from '../types';
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// Resources
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
const profilesDashboard: ResourceDashboardConfig = {
|
|
16
|
+
title: 'User Growth',
|
|
17
|
+
timestampColumn: 'created_at',
|
|
18
|
+
defaultHours: 720, // 30 days
|
|
19
|
+
bucketSize: 'day',
|
|
20
|
+
stats: [
|
|
21
|
+
{ id: 'total', label: 'Users', color: '#1d9bf0', value: { type: 'count' } },
|
|
22
|
+
{ id: 'posts', label: 'Posts', color: '#00ba7c', value: { type: 'table_count', table: 'posts', timestampColumn: 'created_at' } },
|
|
23
|
+
{ id: 'engagement', label: 'Likes', color: '#f91880', value: { type: 'table_count', table: 'likes', timestampColumn: 'created_at' } },
|
|
24
|
+
{ id: 'connections', label: 'Follows', color: '#7856ff', value: { type: 'table_count', table: 'follows', timestampColumn: 'created_at' } },
|
|
25
|
+
],
|
|
26
|
+
dimensions: [],
|
|
27
|
+
listColumns: ['created_at', 'username', 'display_name'],
|
|
28
|
+
searchColumns: ['username', 'display_name'],
|
|
29
|
+
linkToDetail: true,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const profilesResource: ResourceConfig = {
|
|
33
|
+
name: 'profiles',
|
|
34
|
+
label: 'Profile',
|
|
35
|
+
labelPlural: 'Profiles',
|
|
36
|
+
icon: 'person',
|
|
37
|
+
primaryKey: 'id',
|
|
38
|
+
actions: { list: true, show: true, create: false, edit: true, delete: false },
|
|
39
|
+
fields: [
|
|
40
|
+
{ name: 'id', type: 'string', label: 'ID' },
|
|
41
|
+
{ name: 'username', type: 'string', label: 'Username', sortable: true, searchable: true },
|
|
42
|
+
{ name: 'display_name', type: 'string', label: 'Display Name', sortable: true, searchable: true },
|
|
43
|
+
{ name: 'bio', type: 'string', label: 'Bio' },
|
|
44
|
+
{ name: 'avatar_url', type: 'image', label: 'Avatar', renderer: 'avatar' },
|
|
45
|
+
{ name: 'created_at', type: 'datetime', label: 'Joined', sortable: true },
|
|
46
|
+
],
|
|
47
|
+
listFields: ['username', 'display_name', 'bio', 'created_at'],
|
|
48
|
+
editFields: ['username', 'display_name', 'bio', 'avatar_url'],
|
|
49
|
+
defaultSort: { field: 'created_at', order: 'desc' },
|
|
50
|
+
dashboard: profilesDashboard,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const postsDashboard: ResourceDashboardConfig = {
|
|
54
|
+
title: 'Content Velocity',
|
|
55
|
+
timestampColumn: 'created_at',
|
|
56
|
+
defaultHours: 720, // 30 days
|
|
57
|
+
bucketSize: 'day',
|
|
58
|
+
stats: [
|
|
59
|
+
{ id: 'total', label: 'Posts', color: '#0f172a', value: { type: 'count' } },
|
|
60
|
+
{ id: 'authors', label: 'Authors', color: '#1d9bf0', value: { type: 'distinct', column: 'author_id' } },
|
|
61
|
+
{ id: 'engagement', label: 'Likes', color: '#f91880', value: { type: 'table_count', table: 'likes', timestampColumn: 'created_at' } },
|
|
62
|
+
],
|
|
63
|
+
dimensions: [],
|
|
64
|
+
listColumns: ['created_at', 'author_id', 'body'],
|
|
65
|
+
searchColumns: ['body'],
|
|
66
|
+
linkToDetail: true,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const postsResource: ResourceConfig = {
|
|
70
|
+
name: 'posts',
|
|
71
|
+
label: 'Post',
|
|
72
|
+
labelPlural: 'Posts',
|
|
73
|
+
icon: 'chatbubble',
|
|
74
|
+
primaryKey: 'id',
|
|
75
|
+
actions: { list: true, show: true, create: false, edit: false, delete: true },
|
|
76
|
+
fields: [
|
|
77
|
+
{ name: 'id', type: 'string', label: 'ID' },
|
|
78
|
+
{
|
|
79
|
+
name: 'author_id',
|
|
80
|
+
type: 'relation',
|
|
81
|
+
label: 'Author',
|
|
82
|
+
relation: { resource: 'profiles', field: 'id', displayField: 'username' },
|
|
83
|
+
},
|
|
84
|
+
{ name: 'body', type: 'string', label: 'Body', searchable: true },
|
|
85
|
+
{ name: 'created_at', type: 'datetime', label: 'Posted', sortable: true },
|
|
86
|
+
],
|
|
87
|
+
listFields: ['author_id', 'body', 'created_at'],
|
|
88
|
+
defaultSort: { field: 'created_at', order: 'desc' },
|
|
89
|
+
dashboard: postsDashboard,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const likesDashboard: ResourceDashboardConfig = {
|
|
93
|
+
title: 'Engagement',
|
|
94
|
+
timestampColumn: 'created_at',
|
|
95
|
+
defaultHours: 720, // 30 days
|
|
96
|
+
bucketSize: 'day',
|
|
97
|
+
primaryKey: ['user_id', 'post_id'],
|
|
98
|
+
stats: [
|
|
99
|
+
{ id: 'total', label: 'Likes', color: '#f91880', value: { type: 'count' } },
|
|
100
|
+
{ id: 'users', label: 'Likers', color: '#1d9bf0', value: { type: 'distinct', column: 'user_id' } },
|
|
101
|
+
{ id: 'posts', label: 'Posts Liked', color: '#00ba7c', value: { type: 'distinct', column: 'post_id' } },
|
|
102
|
+
],
|
|
103
|
+
dimensions: [],
|
|
104
|
+
listColumns: ['created_at', 'user_id', 'post_id'],
|
|
105
|
+
linkToDetail: false,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const likesResource: ResourceConfig = {
|
|
109
|
+
name: 'likes',
|
|
110
|
+
label: 'Like',
|
|
111
|
+
labelPlural: 'Likes',
|
|
112
|
+
icon: 'heart',
|
|
113
|
+
primaryKey: 'user_id',
|
|
114
|
+
actions: { list: true, show: false, create: false, edit: false, delete: false },
|
|
115
|
+
fields: [
|
|
116
|
+
{
|
|
117
|
+
name: 'user_id',
|
|
118
|
+
type: 'relation',
|
|
119
|
+
label: 'User',
|
|
120
|
+
relation: { resource: 'profiles', field: 'id', displayField: 'username' },
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'post_id',
|
|
124
|
+
type: 'relation',
|
|
125
|
+
label: 'Post',
|
|
126
|
+
relation: { resource: 'posts', field: 'id' },
|
|
127
|
+
},
|
|
128
|
+
{ name: 'created_at', type: 'datetime', label: 'Liked At', sortable: true },
|
|
129
|
+
],
|
|
130
|
+
listFields: ['user_id', 'post_id', 'created_at'],
|
|
131
|
+
defaultSort: { field: 'created_at', order: 'desc' },
|
|
132
|
+
dashboard: likesDashboard,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const followsDashboard: ResourceDashboardConfig = {
|
|
136
|
+
title: 'Network Growth',
|
|
137
|
+
timestampColumn: 'created_at',
|
|
138
|
+
defaultHours: 720, // 30 days
|
|
139
|
+
bucketSize: 'day',
|
|
140
|
+
primaryKey: ['follower_id', 'following_id'],
|
|
141
|
+
stats: [
|
|
142
|
+
{ id: 'total', label: 'Follows', color: '#7856ff', value: { type: 'count' } },
|
|
143
|
+
{ id: 'followers', label: 'Followers', color: '#1d9bf0', value: { type: 'distinct', column: 'follower_id' } },
|
|
144
|
+
{ id: 'following', label: 'Following', color: '#00ba7c', value: { type: 'distinct', column: 'following_id' } },
|
|
145
|
+
],
|
|
146
|
+
dimensions: [],
|
|
147
|
+
listColumns: ['created_at', 'follower_id', 'following_id'],
|
|
148
|
+
linkToDetail: false,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const followsResource: ResourceConfig = {
|
|
152
|
+
name: 'follows',
|
|
153
|
+
label: 'Follow',
|
|
154
|
+
labelPlural: 'Follows',
|
|
155
|
+
icon: 'people',
|
|
156
|
+
primaryKey: 'follower_id',
|
|
157
|
+
actions: { list: true, show: false, create: false, edit: false, delete: false },
|
|
158
|
+
fields: [
|
|
159
|
+
{
|
|
160
|
+
name: 'follower_id',
|
|
161
|
+
type: 'relation',
|
|
162
|
+
label: 'Follower',
|
|
163
|
+
relation: { resource: 'profiles', field: 'id', displayField: 'username' },
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: 'following_id',
|
|
167
|
+
type: 'relation',
|
|
168
|
+
label: 'Following',
|
|
169
|
+
relation: { resource: 'profiles', field: 'id', displayField: 'username' },
|
|
170
|
+
},
|
|
171
|
+
{ name: 'created_at', type: 'datetime', label: 'Followed At', sortable: true },
|
|
172
|
+
],
|
|
173
|
+
listFields: ['follower_id', 'following_id', 'created_at'],
|
|
174
|
+
defaultSort: { field: 'created_at', order: 'desc' },
|
|
175
|
+
dashboard: followsDashboard,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// =============================================================================
|
|
179
|
+
// Dashboard
|
|
180
|
+
// =============================================================================
|
|
181
|
+
|
|
182
|
+
const overviewDashboard: DashboardConfig = {
|
|
183
|
+
id: 'overview',
|
|
184
|
+
title: 'Overview',
|
|
185
|
+
description: 'Platform activity at a glance',
|
|
186
|
+
refreshInterval: 60,
|
|
187
|
+
widgets: [
|
|
188
|
+
{
|
|
189
|
+
id: 'total-users',
|
|
190
|
+
type: 'stat',
|
|
191
|
+
title: 'Total Users',
|
|
192
|
+
data: { resource: 'profiles', aggregation: 'count' },
|
|
193
|
+
grid: { x: 0, y: 0, width: 1, height: 1 },
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: 'total-posts',
|
|
197
|
+
type: 'stat',
|
|
198
|
+
title: 'Total Posts',
|
|
199
|
+
data: { resource: 'posts', aggregation: 'count' },
|
|
200
|
+
grid: { x: 1, y: 0, width: 1, height: 1 },
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: 'total-likes',
|
|
204
|
+
type: 'stat',
|
|
205
|
+
title: 'Total Likes',
|
|
206
|
+
data: { resource: 'likes', aggregation: 'count' },
|
|
207
|
+
grid: { x: 2, y: 0, width: 1, height: 1 },
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: 'total-follows',
|
|
211
|
+
type: 'stat',
|
|
212
|
+
title: 'Total Follows',
|
|
213
|
+
data: { resource: 'follows', aggregation: 'count' },
|
|
214
|
+
grid: { x: 3, y: 0, width: 1, height: 1 },
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// =============================================================================
|
|
220
|
+
// Exported Config
|
|
221
|
+
// =============================================================================
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Twitter Admin Configuration
|
|
225
|
+
*
|
|
226
|
+
* Pre-built configuration for the example Twitter clone app.
|
|
227
|
+
* Pass this to AdminRoot or AdminConfigProvider.
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```tsx
|
|
231
|
+
* import { AdminRoot, EverystackAdapter } from 'everystack/admin';
|
|
232
|
+
* import { twitterAdminConfig } from 'everystack/admin/presets/twitter';
|
|
233
|
+
*
|
|
234
|
+
* const adapter = new EverystackAdapter({ baseUrl: '/api' });
|
|
235
|
+
*
|
|
236
|
+
* export default function AdminLayout() {
|
|
237
|
+
* return (
|
|
238
|
+
* <AdminRoot config={twitterAdminConfig} adapter={adapter}>
|
|
239
|
+
* <Slot />
|
|
240
|
+
* </AdminRoot>
|
|
241
|
+
* );
|
|
242
|
+
* }
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
245
|
+
export const twitterAdminConfig: AdminConfig = {
|
|
246
|
+
title: 'Twitter Admin',
|
|
247
|
+
subtitle: 'Content & User Management',
|
|
248
|
+
basePath: '/admin',
|
|
249
|
+
theme: {
|
|
250
|
+
primaryColor: '#1d9bf0',
|
|
251
|
+
backgroundColor: '#f7f9f9',
|
|
252
|
+
textColor: '#0f1419',
|
|
253
|
+
accentColor: '#1d9bf0',
|
|
254
|
+
},
|
|
255
|
+
resources: [profilesResource, postsResource, likesResource, followsResource],
|
|
256
|
+
dashboards: [overviewDashboard],
|
|
257
|
+
menu: [
|
|
258
|
+
{ id: 'overview', label: 'Overview', dashboard: 'overview' },
|
|
259
|
+
{ id: 'profiles', label: 'Users', icon: 'person', resource: 'profiles' },
|
|
260
|
+
{ id: 'posts', label: 'Posts', icon: 'chatbubble', resource: 'posts' },
|
|
261
|
+
{ id: 'likes', label: 'Likes', icon: 'heart', resource: 'likes' },
|
|
262
|
+
{ id: 'follows', label: 'Follows', icon: 'people', resource: 'follows' },
|
|
263
|
+
],
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/** Individual resource configs for selective use */
|
|
267
|
+
export { profilesResource, postsResource, likesResource, followsResource };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AdminConfigProvider
|
|
3
|
+
*
|
|
4
|
+
* Provides AdminConfig to the admin component tree.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import type { AdminConfig } from '../types';
|
|
9
|
+
import { AdminConfigContext } from '../hooks/useAdminConfig';
|
|
10
|
+
|
|
11
|
+
export interface AdminConfigProviderProps {
|
|
12
|
+
config: AdminConfig;
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function AdminConfigProvider({ config, children }: AdminConfigProviderProps) {
|
|
17
|
+
return <AdminConfigContext.Provider value={{ config }}>{children}</AdminConfigContext.Provider>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DataAdapterProvider
|
|
3
|
+
*
|
|
4
|
+
* Provides the DataAdapter to the admin component tree.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import type { DataAdapter } from '../adapters/types';
|
|
9
|
+
import { DataAdapterContext } from '../hooks/useAdminData';
|
|
10
|
+
|
|
11
|
+
export interface DataAdapterProviderProps {
|
|
12
|
+
adapter: DataAdapter;
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function DataAdapterProvider({ adapter, children }: DataAdapterProviderProps) {
|
|
17
|
+
return <DataAdapterContext.Provider value={{ adapter }}>{children}</DataAdapterContext.Provider>;
|
|
18
|
+
}
|