tangled
alpha
login
or
join now
slices.network
/
slices
137
fork
atom
Highly ambitious ATProtocol AppView service and sdks
137
fork
atom
overview
issues
10
pulls
3
pipelines
add sparklines to slice cards
chadtmiller.com
6 months ago
6be65b76
b85f0579
+517
-286
14 changed files
expand all
collapse all
unified
split
api
.sqlx
query-644745eb5e81daafd5f77110aea2c812d0d4cbd60d69f98d29c90af8ddb350f4.json
query-fed4c0570726ac2aef29d401e041c27fcdded2fc5a98438dd396b0c9d9b7f2fd.json
scripts
generate_typescript.ts
src
database.rs
handler_sparkline.rs
main.rs
models.rs
frontend
src
client.ts
features
dashboard
templates
DashboardPage.tsx
landing
templates
LandingPage.tsx
lib
api.ts
shared
fragments
ActivitySparkline.tsx
SliceCard.tsx
lexicons
network
slices
slice
defs.json
-90
api/.sqlx/query-644745eb5e81daafd5f77110aea2c812d0d4cbd60d69f98d29c90af8ddb350f4.json
···
1
1
-
{
2
2
-
"db_name": "PostgreSQL",
3
3
-
"query": "\n SELECT \n job_id, user_did, slice_uri, status, success, total_records,\n collections_synced, repos_processed, message, error_message,\n created_at, completed_at\n FROM job_results \n WHERE user_did = $1 AND slice_uri = $2\n ORDER BY created_at DESC\n LIMIT $3\n ",
4
4
-
"describe": {
5
5
-
"columns": [
6
6
-
{
7
7
-
"ordinal": 0,
8
8
-
"name": "job_id",
9
9
-
"type_info": "Uuid"
10
10
-
},
11
11
-
{
12
12
-
"ordinal": 1,
13
13
-
"name": "user_did",
14
14
-
"type_info": "Text"
15
15
-
},
16
16
-
{
17
17
-
"ordinal": 2,
18
18
-
"name": "slice_uri",
19
19
-
"type_info": "Text"
20
20
-
},
21
21
-
{
22
22
-
"ordinal": 3,
23
23
-
"name": "status",
24
24
-
"type_info": "Text"
25
25
-
},
26
26
-
{
27
27
-
"ordinal": 4,
28
28
-
"name": "success",
29
29
-
"type_info": "Bool"
30
30
-
},
31
31
-
{
32
32
-
"ordinal": 5,
33
33
-
"name": "total_records",
34
34
-
"type_info": "Int8"
35
35
-
},
36
36
-
{
37
37
-
"ordinal": 6,
38
38
-
"name": "collections_synced",
39
39
-
"type_info": "Jsonb"
40
40
-
},
41
41
-
{
42
42
-
"ordinal": 7,
43
43
-
"name": "repos_processed",
44
44
-
"type_info": "Int8"
45
45
-
},
46
46
-
{
47
47
-
"ordinal": 8,
48
48
-
"name": "message",
49
49
-
"type_info": "Text"
50
50
-
},
51
51
-
{
52
52
-
"ordinal": 9,
53
53
-
"name": "error_message",
54
54
-
"type_info": "Text"
55
55
-
},
56
56
-
{
57
57
-
"ordinal": 10,
58
58
-
"name": "created_at",
59
59
-
"type_info": "Timestamptz"
60
60
-
},
61
61
-
{
62
62
-
"ordinal": 11,
63
63
-
"name": "completed_at",
64
64
-
"type_info": "Timestamptz"
65
65
-
}
66
66
-
],
67
67
-
"parameters": {
68
68
-
"Left": [
69
69
-
"Text",
70
70
-
"Text",
71
71
-
"Int8"
72
72
-
]
73
73
-
},
74
74
-
"nullable": [
75
75
-
false,
76
76
-
false,
77
77
-
false,
78
78
-
false,
79
79
-
false,
80
80
-
false,
81
81
-
false,
82
82
-
false,
83
83
-
false,
84
84
-
true,
85
85
-
false,
86
86
-
false
87
87
-
]
88
88
-
},
89
89
-
"hash": "644745eb5e81daafd5f77110aea2c812d0d4cbd60d69f98d29c90af8ddb350f4"
90
90
-
}
+96
api/.sqlx/query-fed4c0570726ac2aef29d401e041c27fcdded2fc5a98438dd396b0c9d9b7f2fd.json
···
1
1
+
{
2
2
+
"db_name": "PostgreSQL",
3
3
+
"query": "\n -- Completed jobs from job_results\n SELECT\n job_id, user_did, slice_uri, status, success, total_records,\n collections_synced, repos_processed, message, error_message,\n created_at, completed_at,\n 'completed' as job_type\n FROM job_results\n WHERE user_did = $1 AND slice_uri = $2\n\n UNION ALL\n\n -- Pending jobs from message queue\n SELECT\n (p.payload_json->>'job_id')::uuid as job_id,\n p.payload_json->>'user_did' as user_did,\n p.payload_json->>'slice_uri' as slice_uri,\n 'running' as status,\n NULL::boolean as success,\n NULL::bigint as total_records,\n '[]'::jsonb as collections_synced,\n NULL::bigint as repos_processed,\n 'Job in progress...' as message,\n NULL::text as error_message,\n m.created_at,\n NULL::timestamptz as completed_at,\n 'pending' as job_type\n FROM mq_msgs m\n JOIN mq_payloads p ON m.id = p.id\n WHERE m.channel_name = 'sync_queue'\n AND m.id != '00000000-0000-0000-0000-000000000000'\n AND p.payload_json->>'user_did' = $1\n AND p.payload_json->>'slice_uri' = $2\n AND NOT EXISTS (\n SELECT 1 FROM job_results jr\n WHERE jr.job_id = (p.payload_json->>'job_id')::uuid\n )\n\n ORDER BY created_at DESC\n LIMIT $3\n ",
4
4
+
"describe": {
5
5
+
"columns": [
6
6
+
{
7
7
+
"ordinal": 0,
8
8
+
"name": "job_id",
9
9
+
"type_info": "Uuid"
10
10
+
},
11
11
+
{
12
12
+
"ordinal": 1,
13
13
+
"name": "user_did",
14
14
+
"type_info": "Text"
15
15
+
},
16
16
+
{
17
17
+
"ordinal": 2,
18
18
+
"name": "slice_uri",
19
19
+
"type_info": "Text"
20
20
+
},
21
21
+
{
22
22
+
"ordinal": 3,
23
23
+
"name": "status",
24
24
+
"type_info": "Text"
25
25
+
},
26
26
+
{
27
27
+
"ordinal": 4,
28
28
+
"name": "success",
29
29
+
"type_info": "Bool"
30
30
+
},
31
31
+
{
32
32
+
"ordinal": 5,
33
33
+
"name": "total_records",
34
34
+
"type_info": "Int8"
35
35
+
},
36
36
+
{
37
37
+
"ordinal": 6,
38
38
+
"name": "collections_synced",
39
39
+
"type_info": "Jsonb"
40
40
+
},
41
41
+
{
42
42
+
"ordinal": 7,
43
43
+
"name": "repos_processed",
44
44
+
"type_info": "Int8"
45
45
+
},
46
46
+
{
47
47
+
"ordinal": 8,
48
48
+
"name": "message",
49
49
+
"type_info": "Text"
50
50
+
},
51
51
+
{
52
52
+
"ordinal": 9,
53
53
+
"name": "error_message",
54
54
+
"type_info": "Text"
55
55
+
},
56
56
+
{
57
57
+
"ordinal": 10,
58
58
+
"name": "created_at",
59
59
+
"type_info": "Timestamptz"
60
60
+
},
61
61
+
{
62
62
+
"ordinal": 11,
63
63
+
"name": "completed_at",
64
64
+
"type_info": "Timestamptz"
65
65
+
},
66
66
+
{
67
67
+
"ordinal": 12,
68
68
+
"name": "job_type",
69
69
+
"type_info": "Text"
70
70
+
}
71
71
+
],
72
72
+
"parameters": {
73
73
+
"Left": [
74
74
+
"Text",
75
75
+
"Text",
76
76
+
"Int8"
77
77
+
]
78
78
+
},
79
79
+
"nullable": [
80
80
+
null,
81
81
+
null,
82
82
+
null,
83
83
+
null,
84
84
+
null,
85
85
+
null,
86
86
+
null,
87
87
+
null,
88
88
+
null,
89
89
+
null,
90
90
+
null,
91
91
+
null,
92
92
+
null
93
93
+
]
94
94
+
},
95
95
+
"hash": "fed4c0570726ac2aef29d401e041c27fcdded2fc5a98438dd396b0c9d9b7f2fd"
96
96
+
}
+31
api/scripts/generate_typescript.ts
···
425
425
],
426
426
});
427
427
428
428
+
429
429
+
sourceFile.addInterface({
430
430
+
name: "GetSparklinesParams",
431
431
+
isExported: true,
432
432
+
properties: [
433
433
+
{ name: "slices", type: "string[]" },
434
434
+
{ name: "interval", type: "string", hasQuestionToken: true },
435
435
+
{ name: "duration", type: "string", hasQuestionToken: true },
436
436
+
],
437
437
+
});
438
438
+
439
439
+
sourceFile.addInterface({
440
440
+
name: "GetSparklinesOutput",
441
441
+
isExported: true,
442
442
+
properties: [
443
443
+
{ name: "success", type: "boolean" },
444
444
+
{ name: "sparklines", type: `Record<string, NetworkSlicesSliceDefs["SparklinePoint"][]>` },
445
445
+
{ name: "message", type: "string", hasQuestionToken: true },
446
446
+
],
447
447
+
});
448
448
+
428
449
// These base interfaces are now imported from @slices/client
429
450
430
451
// OAuth client interfaces
···
1216
1237
isAsync: true,
1217
1238
statements: [
1218
1239
`return await this.client.makeRequest<SliceStatsOutput>('network.slices.slice.stats', 'POST', params);`,
1240
1240
+
],
1241
1241
+
});
1242
1242
+
1243
1243
+
classDeclaration.addMethod({
1244
1244
+
name: "getSparklines",
1245
1245
+
parameters: [{ name: "params", type: "GetSparklinesParams" }],
1246
1246
+
returnType: "Promise<GetSparklinesOutput>",
1247
1247
+
isAsync: true,
1248
1248
+
statements: [
1249
1249
+
`return await this.client.makeRequest<GetSparklinesOutput>('network.slices.slice.getSparklines', 'POST', params);`,
1219
1250
],
1220
1251
});
1221
1252
+54
api/src/database.rs
···
1229
1229
1230
1230
Ok(())
1231
1231
}
1232
1232
+
1233
1233
+
1234
1234
+
pub async fn get_batch_sparkline_data(
1235
1235
+
&self,
1236
1236
+
slice_uris: &[String],
1237
1237
+
interval: &str,
1238
1238
+
duration_hours: i32,
1239
1239
+
) -> Result<std::collections::HashMap<String, Vec<crate::models::SparklinePoint>>, DatabaseError> {
1240
1240
+
use chrono::{Duration, Utc};
1241
1241
+
let cutoff_time = Utc::now() - Duration::hours(duration_hours as i64);
1242
1242
+
1243
1243
+
let mut sparklines = std::collections::HashMap::new();
1244
1244
+
1245
1245
+
for slice_uri in slice_uris {
1246
1246
+
// Validate interval to prevent SQL injection
1247
1247
+
let interval_validated = match interval {
1248
1248
+
"minute" => "minute",
1249
1249
+
"day" => "day",
1250
1250
+
_ => "hour",
1251
1251
+
};
1252
1252
+
1253
1253
+
let query = format!(
1254
1254
+
r#"
1255
1255
+
SELECT
1256
1256
+
date_trunc('{}', indexed_at) as bucket,
1257
1257
+
COUNT(*) as count
1258
1258
+
FROM record
1259
1259
+
WHERE indexed_at >= $1
1260
1260
+
AND slice_uri = $2
1261
1261
+
GROUP BY bucket
1262
1262
+
ORDER BY bucket
1263
1263
+
"#,
1264
1264
+
interval_validated
1265
1265
+
);
1266
1266
+
1267
1267
+
let rows = sqlx::query_as::<_, (Option<chrono::DateTime<chrono::Utc>>, Option<i64>)>(&query)
1268
1268
+
.bind(cutoff_time)
1269
1269
+
.bind(slice_uri)
1270
1270
+
.fetch_all(&self.pool)
1271
1271
+
.await?;
1272
1272
+
1273
1273
+
let data_points = rows
1274
1274
+
.into_iter()
1275
1275
+
.map(|(bucket, count)| crate::models::SparklinePoint {
1276
1276
+
timestamp: bucket.unwrap().to_rfc3339(),
1277
1277
+
count: count.unwrap_or(0),
1278
1278
+
})
1279
1279
+
.collect();
1280
1280
+
1281
1281
+
sparklines.insert(slice_uri.clone(), data_points);
1282
1282
+
}
1283
1283
+
1284
1284
+
Ok(sparklines)
1285
1285
+
}
1232
1286
}
+50
api/src/handler_sparkline.rs
···
1
1
+
use axum::{
2
2
+
extract::State,
3
3
+
http::StatusCode,
4
4
+
response::Json,
5
5
+
};
6
6
+
use crate::models::{GetSparklinesParams, GetSparklinesOutput};
7
7
+
use crate::AppState;
8
8
+
9
9
+
pub async fn batch_sparkline(
10
10
+
State(state): State<AppState>,
11
11
+
axum::extract::Json(params): axum::extract::Json<GetSparklinesParams>,
12
12
+
) -> Result<Json<GetSparklinesOutput>, StatusCode> {
13
13
+
match get_batch_sparkline_data(&state, ¶ms).await {
14
14
+
Ok(output) => Ok(Json(output)),
15
15
+
Err(_) => Ok(Json(GetSparklinesOutput {
16
16
+
success: false,
17
17
+
sparklines: std::collections::HashMap::new(),
18
18
+
message: Some("Failed to get batch sparkline data".to_string()),
19
19
+
})),
20
20
+
}
21
21
+
}
22
22
+
23
23
+
async fn get_batch_sparkline_data(
24
24
+
state: &AppState,
25
25
+
params: &GetSparklinesParams,
26
26
+
) -> Result<GetSparklinesOutput, Box<dyn std::error::Error + Send + Sync>> {
27
27
+
let interval = params.interval.as_deref().unwrap_or("hour");
28
28
+
let duration = params.duration.as_deref().unwrap_or("24h");
29
29
+
30
30
+
// Parse duration
31
31
+
let duration_hours = match duration {
32
32
+
"1h" => 1,
33
33
+
"24h" => 24,
34
34
+
"7d" => 24 * 7,
35
35
+
"30d" => 24 * 30,
36
36
+
_ => 24, // default to 24h
37
37
+
};
38
38
+
39
39
+
let sparklines = state.database.get_batch_sparkline_data(
40
40
+
¶ms.slices,
41
41
+
interval,
42
42
+
duration_hours,
43
43
+
).await?;
44
44
+
45
45
+
Ok(GetSparklinesOutput {
46
46
+
success: true,
47
47
+
sparklines,
48
48
+
message: None,
49
49
+
})
50
50
+
}
+5
api/src/main.rs
···
10
10
mod handler_logs;
11
11
mod handler_oauth_clients;
12
12
mod handler_openapi_spec;
13
13
+
mod handler_sparkline;
13
14
mod handler_stats;
14
15
mod handler_sync;
15
16
mod handler_sync_user_collections;
···
346
347
.route(
347
348
"/xrpc/network.slices.slice.stats",
348
349
post(handler_stats::stats),
350
350
+
)
351
351
+
.route(
352
352
+
"/xrpc/network.slices.slice.getSparklines",
353
353
+
post(handler_sparkline::batch_sparkline),
349
354
)
350
355
.route(
351
356
"/xrpc/network.slices.slice.getSliceRecords",
+24
api/src/models.rs
···
203
203
pub success: bool,
204
204
pub message: String,
205
205
}
206
206
+
207
207
+
208
208
+
#[derive(Debug, Serialize, Deserialize)]
209
209
+
#[serde(rename_all = "camelCase")]
210
210
+
pub struct SparklinePoint {
211
211
+
pub timestamp: String, // ISO 8601
212
212
+
pub count: i64,
213
213
+
}
214
214
+
215
215
+
#[derive(Debug, Serialize, Deserialize)]
216
216
+
#[serde(rename_all = "camelCase")]
217
217
+
pub struct GetSparklinesParams {
218
218
+
pub slices: Vec<String>,
219
219
+
pub interval: Option<String>, // "hour", "day", "minute" - defaults to "hour"
220
220
+
pub duration: Option<String>, // "24h", "7d", "30d" - defaults to "24h"
221
221
+
}
222
222
+
223
223
+
#[derive(Debug, Serialize, Deserialize)]
224
224
+
#[serde(rename_all = "camelCase")]
225
225
+
pub struct GetSparklinesOutput {
226
226
+
pub success: bool,
227
227
+
pub sparklines: std::collections::HashMap<String, Vec<SparklinePoint>>,
228
228
+
pub message: Option<String>,
229
229
+
}
+142
-153
frontend/src/client.ts
···
1
1
// Generated TypeScript client for AT Protocol records
2
2
-
// Generated at: 2025-09-14 17:24:23 UTC
2
2
+
// Generated at: 2025-09-15 21:45:37 UTC
3
3
// Lexicons: 9
4
4
5
5
/**
···
198
198
message?: string;
199
199
}
200
200
201
201
+
export interface GetSparklinesParams {
202
202
+
slices: string[];
203
203
+
interval?: string;
204
204
+
duration?: string;
205
205
+
}
206
206
+
207
207
+
export interface GetSparklinesOutput {
208
208
+
success: boolean;
209
209
+
sparklines: Record<string, NetworkSlicesSliceDefs["SparklinePoint"][]>;
210
210
+
message?: string;
211
211
+
}
212
212
+
201
213
export interface CreateOAuthClientRequest {
202
214
clientName: string;
203
215
redirectUris: string[];
···
255
267
labels?:
256
268
| ComAtprotoLabelDefs["SelfLabels"]
257
269
| {
258
258
-
$type: string;
259
259
-
[key: string]: unknown;
260
260
-
};
270
270
+
$type: string;
271
271
+
[key: string]: unknown;
272
272
+
};
261
273
createdAt?: string;
262
274
pinnedPost?: ComAtprotoRepoStrongRef;
263
275
/** Free-form profile description text. */
···
281
293
/** Profile of the slice creator */
282
294
creator: NetworkSlicesActorDefs["ProfileViewBasic"];
283
295
createdAt: string;
296
296
+
/** Recent activity sparkline data points for the last 24 hours */
297
297
+
sparkline?: NetworkSlicesSliceDefs["SparklinePoint"][];
298
298
+
}
299
299
+
300
300
+
export interface NetworkSlicesSliceDefsSparklinePoint {
301
301
+
timestamp: string;
302
302
+
count: number;
284
303
}
285
304
286
305
export interface NetworkSlicesSlice {
···
404
423
405
424
export interface NetworkSlicesSliceDefs {
406
425
readonly SliceView: NetworkSlicesSliceDefsSliceView;
426
426
+
readonly SparklinePoint: NetworkSlicesSliceDefsSparklinePoint;
407
427
}
408
428
409
429
export interface NetworkSlicesActorDefs {
···
415
435
readonly SelfLabel: ComAtprotoLabelDefsSelfLabel;
416
436
readonly SelfLabels: ComAtprotoLabelDefsSelfLabels;
417
437
readonly LabelValueDefinition: ComAtprotoLabelDefsLabelValueDefinition;
418
418
-
readonly LabelValueDefinitionStrings:
419
419
-
ComAtprotoLabelDefsLabelValueDefinitionStrings;
438
438
+
readonly LabelValueDefinitionStrings: ComAtprotoLabelDefsLabelValueDefinitionStrings;
420
439
}
421
440
422
441
class ProfileActorBskyAppClient {
···
430
449
limit?: number;
431
450
cursor?: string;
432
451
where?: {
433
433
-
[
434
434
-
K in
435
435
-
| AppBskyActorProfileSortFields
436
436
-
| IndexedRecordFields
437
437
-
]?: WhereCondition;
452
452
+
[K in
453
453
+
| AppBskyActorProfileSortFields
454
454
+
| IndexedRecordFields]?: WhereCondition;
438
455
};
439
456
orWhere?: {
440
440
-
[
441
441
-
K in
442
442
-
| AppBskyActorProfileSortFields
443
443
-
| IndexedRecordFields
444
444
-
]?: WhereCondition;
457
457
+
[K in
458
458
+
| AppBskyActorProfileSortFields
459
459
+
| IndexedRecordFields]?: WhereCondition;
445
460
};
446
461
sortBy?: SortField<AppBskyActorProfileSortFields>[];
447
462
}): Promise<GetRecordsResponse<AppBskyActorProfile>> {
···
449
464
}
450
465
451
466
async getRecord(
452
452
-
params: GetRecordParams,
467
467
+
params: GetRecordParams
453
468
): Promise<RecordResponse<AppBskyActorProfile>> {
454
469
return await this.client.getRecord("app.bsky.actor.profile", params);
455
470
}
···
458
473
limit?: number;
459
474
cursor?: string;
460
475
where?: {
461
461
-
[
462
462
-
K in
463
463
-
| AppBskyActorProfileSortFields
464
464
-
| IndexedRecordFields
465
465
-
]?: WhereCondition;
476
476
+
[K in
477
477
+
| AppBskyActorProfileSortFields
478
478
+
| IndexedRecordFields]?: WhereCondition;
466
479
};
467
480
orWhere?: {
468
468
-
[
469
469
-
K in
470
470
-
| AppBskyActorProfileSortFields
471
471
-
| IndexedRecordFields
472
472
-
]?: WhereCondition;
481
481
+
[K in
482
482
+
| AppBskyActorProfileSortFields
483
483
+
| IndexedRecordFields]?: WhereCondition;
473
484
};
474
485
sortBy?: SortField<AppBskyActorProfileSortFields>[];
475
486
}): Promise<CountRecordsResponse> {
···
478
489
479
490
async createRecord(
480
491
record: AppBskyActorProfile,
481
481
-
useSelfRkey?: boolean,
492
492
+
useSelfRkey?: boolean
482
493
): Promise<{ uri: string; cid: string }> {
483
494
return await this.client.createRecord(
484
495
"app.bsky.actor.profile",
485
496
record,
486
486
-
useSelfRkey,
497
497
+
useSelfRkey
487
498
);
488
499
}
489
500
490
501
async updateRecord(
491
502
rkey: string,
492
492
-
record: AppBskyActorProfile,
503
503
+
record: AppBskyActorProfile
493
504
): Promise<{ uri: string; cid: string }> {
494
505
return await this.client.updateRecord(
495
506
"app.bsky.actor.profile",
496
507
rkey,
497
497
-
record,
508
508
+
record
498
509
);
499
510
}
500
511
···
544
555
limit?: number;
545
556
cursor?: string;
546
557
where?: {
547
547
-
[
548
548
-
K in
549
549
-
| NetworkSlicesSliceSortFields
550
550
-
| IndexedRecordFields
551
551
-
]?: WhereCondition;
558
558
+
[K in
559
559
+
| NetworkSlicesSliceSortFields
560
560
+
| IndexedRecordFields]?: WhereCondition;
552
561
};
553
562
orWhere?: {
554
554
-
[
555
555
-
K in
556
556
-
| NetworkSlicesSliceSortFields
557
557
-
| IndexedRecordFields
558
558
-
]?: WhereCondition;
563
563
+
[K in
564
564
+
| NetworkSlicesSliceSortFields
565
565
+
| IndexedRecordFields]?: WhereCondition;
559
566
};
560
567
sortBy?: SortField<NetworkSlicesSliceSortFields>[];
561
568
}): Promise<GetRecordsResponse<NetworkSlicesSlice>> {
···
563
570
}
564
571
565
572
async getRecord(
566
566
-
params: GetRecordParams,
573
573
+
params: GetRecordParams
567
574
): Promise<RecordResponse<NetworkSlicesSlice>> {
568
575
return await this.client.getRecord("network.slices.slice", params);
569
576
}
···
572
579
limit?: number;
573
580
cursor?: string;
574
581
where?: {
575
575
-
[
576
576
-
K in
577
577
-
| NetworkSlicesSliceSortFields
578
578
-
| IndexedRecordFields
579
579
-
]?: WhereCondition;
582
582
+
[K in
583
583
+
| NetworkSlicesSliceSortFields
584
584
+
| IndexedRecordFields]?: WhereCondition;
580
585
};
581
586
orWhere?: {
582
582
-
[
583
583
-
K in
584
584
-
| NetworkSlicesSliceSortFields
585
585
-
| IndexedRecordFields
586
586
-
]?: WhereCondition;
587
587
+
[K in
588
588
+
| NetworkSlicesSliceSortFields
589
589
+
| IndexedRecordFields]?: WhereCondition;
587
590
};
588
591
sortBy?: SortField<NetworkSlicesSliceSortFields>[];
589
592
}): Promise<CountRecordsResponse> {
···
592
595
593
596
async createRecord(
594
597
record: NetworkSlicesSlice,
595
595
-
useSelfRkey?: boolean,
598
598
+
useSelfRkey?: boolean
596
599
): Promise<{ uri: string; cid: string }> {
597
600
return await this.client.createRecord(
598
601
"network.slices.slice",
599
602
record,
600
600
-
useSelfRkey,
603
603
+
useSelfRkey
601
604
);
602
605
}
603
606
604
607
async updateRecord(
605
608
rkey: string,
606
606
-
record: NetworkSlicesSlice,
609
609
+
record: NetworkSlicesSlice
607
610
): Promise<{ uri: string; cid: string }> {
608
611
return await this.client.updateRecord("network.slices.slice", rkey, record);
609
612
}
···
616
619
return await this.client.makeRequest<CodegenXrpcResponse>(
617
620
"network.slices.slice.codegen",
618
621
"POST",
619
619
-
request,
622
622
+
request
620
623
);
621
624
}
622
625
···
624
627
return await this.client.makeRequest<SliceStatsOutput>(
625
628
"network.slices.slice.stats",
626
629
"POST",
627
627
-
params,
630
630
+
params
631
631
+
);
632
632
+
}
633
633
+
634
634
+
async getSparklines(
635
635
+
params: GetSparklinesParams
636
636
+
): Promise<GetSparklinesOutput> {
637
637
+
return await this.client.makeRequest<GetSparklinesOutput>(
638
638
+
"network.slices.slice.getSparklines",
639
639
+
"POST",
640
640
+
params
628
641
);
629
642
}
630
643
631
644
async getSliceRecords<T = Record<string, unknown>>(
632
632
-
params: Omit<SliceLevelRecordsParams<T>, "slice">,
645
645
+
params: Omit<SliceLevelRecordsParams<T>, "slice">
633
646
): Promise<SliceRecordsOutput<T>> {
634
647
// Combine where and orWhere into the expected backend format
635
648
const whereClause: any = params?.where ? { ...params.where } : {};
···
646
659
return await this.client.makeRequest<SliceRecordsOutput<T>>(
647
660
"network.slices.slice.getSliceRecords",
648
661
"POST",
649
649
-
requestParams,
662
662
+
requestParams
650
663
);
651
664
}
652
665
···
655
668
return await this.client.makeRequest<GetActorsResponse>(
656
669
"network.slices.slice.getActors",
657
670
"POST",
658
658
-
requestParams,
671
671
+
requestParams
659
672
);
660
673
}
661
674
···
664
677
return await this.client.makeRequest<SyncJobResponse>(
665
678
"network.slices.slice.startSync",
666
679
"POST",
667
667
-
requestParams,
680
680
+
requestParams
668
681
);
669
682
}
670
683
···
672
685
return await this.client.makeRequest<JobStatus>(
673
686
"network.slices.slice.getJobStatus",
674
687
"GET",
675
675
-
params,
688
688
+
params
676
689
);
677
690
}
678
691
679
692
async getJobHistory(
680
680
-
params: GetJobHistoryParams,
693
693
+
params: GetJobHistoryParams
681
694
): Promise<GetJobHistoryResponse> {
682
695
return await this.client.makeRequest<GetJobHistoryResponse>(
683
696
"network.slices.slice.getJobHistory",
684
697
"GET",
685
685
-
params,
698
698
+
params
686
699
);
687
700
}
688
701
···
690
703
return await this.client.makeRequest<GetJobLogsResponse>(
691
704
"network.slices.slice.getJobLogs",
692
705
"GET",
693
693
-
params,
706
706
+
params
694
707
);
695
708
}
696
709
697
710
async getJetstreamStatus(): Promise<JetstreamStatusResponse> {
698
711
return await this.client.makeRequest<JetstreamStatusResponse>(
699
712
"network.slices.slice.getJetstreamStatus",
700
700
-
"GET",
713
713
+
"GET"
701
714
);
702
715
}
703
716
704
717
async getJetstreamLogs(
705
705
-
params: GetJetstreamLogsParams,
718
718
+
params: GetJetstreamLogsParams
706
719
): Promise<GetJetstreamLogsResponse> {
707
720
return await this.client.makeRequest<GetJetstreamLogsResponse>(
708
721
"network.slices.slice.getJetstreamLogs",
709
722
"GET",
710
710
-
params,
723
723
+
params
711
724
);
712
725
}
713
726
714
727
async syncUserCollections(
715
715
-
params?: SyncUserCollectionsRequest,
728
728
+
params?: SyncUserCollectionsRequest
716
729
): Promise<SyncUserCollectionsResult> {
717
730
const requestParams = { slice: this.client.sliceUri, ...params };
718
731
return await this.client.makeRequest<SyncUserCollectionsResult>(
719
732
"network.slices.slice.syncUserCollections",
720
733
"POST",
721
721
-
requestParams,
734
734
+
requestParams
722
735
);
723
736
}
724
737
725
738
async createOAuthClient(
726
726
-
params: CreateOAuthClientRequest,
739
739
+
params: CreateOAuthClientRequest
727
740
): Promise<OAuthClientDetails> {
728
741
const requestParams = { ...params, sliceUri: this.client.sliceUri };
729
742
return await this.client.makeRequest<OAuthClientDetails>(
730
743
"network.slices.slice.createOAuthClient",
731
744
"POST",
732
732
-
requestParams,
745
745
+
requestParams
733
746
);
734
747
}
735
748
···
738
751
return await this.client.makeRequest<ListOAuthClientsResponse>(
739
752
"network.slices.slice.getOAuthClients",
740
753
"GET",
741
741
-
requestParams,
754
754
+
requestParams
742
755
);
743
756
}
744
757
745
758
async updateOAuthClient(
746
746
-
params: UpdateOAuthClientRequest,
759
759
+
params: UpdateOAuthClientRequest
747
760
): Promise<OAuthClientDetails> {
748
761
const requestParams = { ...params, sliceUri: this.client.sliceUri };
749
762
return await this.client.makeRequest<OAuthClientDetails>(
750
763
"network.slices.slice.updateOAuthClient",
751
764
"POST",
752
752
-
requestParams,
765
765
+
requestParams
753
766
);
754
767
}
755
768
756
769
async deleteOAuthClient(
757
757
-
clientId: string,
770
770
+
clientId: string
758
771
): Promise<DeleteOAuthClientResponse> {
759
772
return await this.client.makeRequest<DeleteOAuthClientResponse>(
760
773
"network.slices.slice.deleteOAuthClient",
761
774
"POST",
762
762
-
{ clientId },
775
775
+
{ clientId }
763
776
);
764
777
}
765
778
}
···
775
788
limit?: number;
776
789
cursor?: string;
777
790
where?: {
778
778
-
[
779
779
-
K in
780
780
-
| NetworkSlicesWaitingSortFields
781
781
-
| IndexedRecordFields
782
782
-
]?: WhereCondition;
791
791
+
[K in
792
792
+
| NetworkSlicesWaitingSortFields
793
793
+
| IndexedRecordFields]?: WhereCondition;
783
794
};
784
795
orWhere?: {
785
785
-
[
786
786
-
K in
787
787
-
| NetworkSlicesWaitingSortFields
788
788
-
| IndexedRecordFields
789
789
-
]?: WhereCondition;
796
796
+
[K in
797
797
+
| NetworkSlicesWaitingSortFields
798
798
+
| IndexedRecordFields]?: WhereCondition;
790
799
};
791
800
sortBy?: SortField<NetworkSlicesWaitingSortFields>[];
792
801
}): Promise<GetRecordsResponse<NetworkSlicesWaiting>> {
···
794
803
}
795
804
796
805
async getRecord(
797
797
-
params: GetRecordParams,
806
806
+
params: GetRecordParams
798
807
): Promise<RecordResponse<NetworkSlicesWaiting>> {
799
808
return await this.client.getRecord("network.slices.waiting", params);
800
809
}
···
803
812
limit?: number;
804
813
cursor?: string;
805
814
where?: {
806
806
-
[
807
807
-
K in
808
808
-
| NetworkSlicesWaitingSortFields
809
809
-
| IndexedRecordFields
810
810
-
]?: WhereCondition;
815
815
+
[K in
816
816
+
| NetworkSlicesWaitingSortFields
817
817
+
| IndexedRecordFields]?: WhereCondition;
811
818
};
812
819
orWhere?: {
813
813
-
[
814
814
-
K in
815
815
-
| NetworkSlicesWaitingSortFields
816
816
-
| IndexedRecordFields
817
817
-
]?: WhereCondition;
820
820
+
[K in
821
821
+
| NetworkSlicesWaitingSortFields
822
822
+
| IndexedRecordFields]?: WhereCondition;
818
823
};
819
824
sortBy?: SortField<NetworkSlicesWaitingSortFields>[];
820
825
}): Promise<CountRecordsResponse> {
···
823
828
824
829
async createRecord(
825
830
record: NetworkSlicesWaiting,
826
826
-
useSelfRkey?: boolean,
831
831
+
useSelfRkey?: boolean
827
832
): Promise<{ uri: string; cid: string }> {
828
833
return await this.client.createRecord(
829
834
"network.slices.waiting",
830
835
record,
831
831
-
useSelfRkey,
836
836
+
useSelfRkey
832
837
);
833
838
}
834
839
835
840
async updateRecord(
836
841
rkey: string,
837
837
-
record: NetworkSlicesWaiting,
842
842
+
record: NetworkSlicesWaiting
838
843
): Promise<{ uri: string; cid: string }> {
839
844
return await this.client.updateRecord(
840
845
"network.slices.waiting",
841
846
rkey,
842
842
-
record,
847
847
+
record
843
848
);
844
849
}
845
850
···
859
864
limit?: number;
860
865
cursor?: string;
861
866
where?: {
862
862
-
[
863
863
-
K in
864
864
-
| NetworkSlicesLexiconSortFields
865
865
-
| IndexedRecordFields
866
866
-
]?: WhereCondition;
867
867
+
[K in
868
868
+
| NetworkSlicesLexiconSortFields
869
869
+
| IndexedRecordFields]?: WhereCondition;
867
870
};
868
871
orWhere?: {
869
869
-
[
870
870
-
K in
871
871
-
| NetworkSlicesLexiconSortFields
872
872
-
| IndexedRecordFields
873
873
-
]?: WhereCondition;
872
872
+
[K in
873
873
+
| NetworkSlicesLexiconSortFields
874
874
+
| IndexedRecordFields]?: WhereCondition;
874
875
};
875
876
sortBy?: SortField<NetworkSlicesLexiconSortFields>[];
876
877
}): Promise<GetRecordsResponse<NetworkSlicesLexicon>> {
···
878
879
}
879
880
880
881
async getRecord(
881
881
-
params: GetRecordParams,
882
882
+
params: GetRecordParams
882
883
): Promise<RecordResponse<NetworkSlicesLexicon>> {
883
884
return await this.client.getRecord("network.slices.lexicon", params);
884
885
}
···
887
888
limit?: number;
888
889
cursor?: string;
889
890
where?: {
890
890
-
[
891
891
-
K in
892
892
-
| NetworkSlicesLexiconSortFields
893
893
-
| IndexedRecordFields
894
894
-
]?: WhereCondition;
891
891
+
[K in
892
892
+
| NetworkSlicesLexiconSortFields
893
893
+
| IndexedRecordFields]?: WhereCondition;
895
894
};
896
895
orWhere?: {
897
897
-
[
898
898
-
K in
899
899
-
| NetworkSlicesLexiconSortFields
900
900
-
| IndexedRecordFields
901
901
-
]?: WhereCondition;
896
896
+
[K in
897
897
+
| NetworkSlicesLexiconSortFields
898
898
+
| IndexedRecordFields]?: WhereCondition;
902
899
};
903
900
sortBy?: SortField<NetworkSlicesLexiconSortFields>[];
904
901
}): Promise<CountRecordsResponse> {
···
907
904
908
905
async createRecord(
909
906
record: NetworkSlicesLexicon,
910
910
-
useSelfRkey?: boolean,
907
907
+
useSelfRkey?: boolean
911
908
): Promise<{ uri: string; cid: string }> {
912
909
return await this.client.createRecord(
913
910
"network.slices.lexicon",
914
911
record,
915
915
-
useSelfRkey,
912
912
+
useSelfRkey
916
913
);
917
914
}
918
915
919
916
async updateRecord(
920
917
rkey: string,
921
921
-
record: NetworkSlicesLexicon,
918
918
+
record: NetworkSlicesLexicon
922
919
): Promise<{ uri: string; cid: string }> {
923
920
return await this.client.updateRecord(
924
921
"network.slices.lexicon",
925
922
rkey,
926
926
-
record,
923
923
+
record
927
924
);
928
925
}
929
926
···
943
940
limit?: number;
944
941
cursor?: string;
945
942
where?: {
946
946
-
[
947
947
-
K in
948
948
-
| NetworkSlicesActorProfileSortFields
949
949
-
| IndexedRecordFields
950
950
-
]?: WhereCondition;
943
943
+
[K in
944
944
+
| NetworkSlicesActorProfileSortFields
945
945
+
| IndexedRecordFields]?: WhereCondition;
951
946
};
952
947
orWhere?: {
953
953
-
[
954
954
-
K in
955
955
-
| NetworkSlicesActorProfileSortFields
956
956
-
| IndexedRecordFields
957
957
-
]?: WhereCondition;
948
948
+
[K in
949
949
+
| NetworkSlicesActorProfileSortFields
950
950
+
| IndexedRecordFields]?: WhereCondition;
958
951
};
959
952
sortBy?: SortField<NetworkSlicesActorProfileSortFields>[];
960
953
}): Promise<GetRecordsResponse<NetworkSlicesActorProfile>> {
···
962
955
}
963
956
964
957
async getRecord(
965
965
-
params: GetRecordParams,
958
958
+
params: GetRecordParams
966
959
): Promise<RecordResponse<NetworkSlicesActorProfile>> {
967
960
return await this.client.getRecord("network.slices.actor.profile", params);
968
961
}
···
971
964
limit?: number;
972
965
cursor?: string;
973
966
where?: {
974
974
-
[
975
975
-
K in
976
976
-
| NetworkSlicesActorProfileSortFields
977
977
-
| IndexedRecordFields
978
978
-
]?: WhereCondition;
967
967
+
[K in
968
968
+
| NetworkSlicesActorProfileSortFields
969
969
+
| IndexedRecordFields]?: WhereCondition;
979
970
};
980
971
orWhere?: {
981
981
-
[
982
982
-
K in
983
983
-
| NetworkSlicesActorProfileSortFields
984
984
-
| IndexedRecordFields
985
985
-
]?: WhereCondition;
972
972
+
[K in
973
973
+
| NetworkSlicesActorProfileSortFields
974
974
+
| IndexedRecordFields]?: WhereCondition;
986
975
};
987
976
sortBy?: SortField<NetworkSlicesActorProfileSortFields>[];
988
977
}): Promise<CountRecordsResponse> {
989
978
return await this.client.countRecords(
990
979
"network.slices.actor.profile",
991
991
-
params,
980
980
+
params
992
981
);
993
982
}
994
983
995
984
async createRecord(
996
985
record: NetworkSlicesActorProfile,
997
997
-
useSelfRkey?: boolean,
986
986
+
useSelfRkey?: boolean
998
987
): Promise<{ uri: string; cid: string }> {
999
988
return await this.client.createRecord(
1000
989
"network.slices.actor.profile",
1001
990
record,
1002
1002
-
useSelfRkey,
991
991
+
useSelfRkey
1003
992
);
1004
993
}
1005
994
1006
995
async updateRecord(
1007
996
rkey: string,
1008
1008
-
record: NetworkSlicesActorProfile,
997
997
+
record: NetworkSlicesActorProfile
1009
998
): Promise<{ uri: string; cid: string }> {
1010
999
return await this.client.updateRecord(
1011
1000
"network.slices.actor.profile",
1012
1001
rkey,
1013
1013
-
record,
1002
1002
+
record
1014
1003
);
1015
1004
}
1016
1005
+5
frontend/src/features/dashboard/templates/DashboardPage.tsx
···
64
64
{slices.length > 0
65
65
? (
66
66
<div className="space-y-4">
67
67
+
<div className="flex items-center justify-between mb-4">
68
68
+
<p className="text-sm text-zinc-500">
69
69
+
Activity shows records indexed in the past 24 hours
70
70
+
</p>
71
71
+
</div>
67
72
{slices.map((slice) => (
68
73
<SliceCard
69
74
key={slice.uri}
+5
frontend/src/features/landing/templates/LandingPage.tsx
···
26
26
{slices.length > 0
27
27
? (
28
28
<div className="space-y-4">
29
29
+
<div className="flex items-center justify-between mb-4">
30
30
+
<p className="text-sm text-zinc-500">
31
31
+
Activity shows records indexed in the past 24 hours
32
32
+
</p>
33
33
+
</div>
29
34
{slices.map((slice) => (
30
35
<SliceCard
31
36
key={slice.uri}
+62
-13
frontend/src/lib/api.ts
···
4
4
NetworkSlicesActorProfile,
5
5
NetworkSlicesSlice,
6
6
NetworkSlicesSliceDefsSliceView,
7
7
+
NetworkSlicesSliceDefsSparklinePoint,
7
8
} from "../client.ts";
8
9
import { recordBlobToCdnUrl, RecordResponse } from "@slices/client";
9
10
11
11
+
async function fetchSparklinesForSlices(
12
12
+
client: AtProtoClient,
13
13
+
sliceUris: string[]
14
14
+
): Promise<Record<string, NetworkSlicesSliceDefsSparklinePoint[]>> {
15
15
+
if (sliceUris.length === 0) return {};
16
16
+
17
17
+
try {
18
18
+
const sparklinesResponse = await client.network.slices.slice.getSparklines({
19
19
+
slices: sliceUris,
20
20
+
duration: "24h",
21
21
+
});
22
22
+
if (sparklinesResponse.success) {
23
23
+
return sparklinesResponse.sparklines;
24
24
+
}
25
25
+
} catch (error) {
26
26
+
console.warn("Failed to fetch batch sparkline data:", error);
27
27
+
}
28
28
+
return {};
29
29
+
}
30
30
+
10
31
export async function getSlice(
11
32
client: AtProtoClient,
12
12
-
uri: string,
33
33
+
uri: string
13
34
): Promise<NetworkSlicesSliceDefsSliceView | null> {
14
35
try {
15
36
const sliceRecord = await client.network.slices.slice.getRecord({ uri });
···
18
39
const creatorProfile = await getSliceActor(client, sliceRecord.did);
19
40
if (!creatorProfile) return null;
20
41
21
21
-
return sliceToView(sliceRecord, creatorProfile);
42
42
+
const sparklinesMap = await fetchSparklinesForSlices(client, [uri]);
43
43
+
const sparklineData = sparklinesMap[uri];
44
44
+
45
45
+
return sliceToView(sliceRecord, creatorProfile, sparklineData);
22
46
} catch (error) {
23
47
console.error("Failed to get slice:", error);
24
48
return null;
···
27
51
28
52
export async function getSliceActor(
29
53
client: AtProtoClient,
30
30
-
did: string,
54
54
+
did: string
31
55
): Promise<NetworkSlicesActorDefsProfileViewBasic | null> {
32
56
try {
33
57
const profileUri = `at://${did}/network.slices.actor.profile/self`;
···
42
66
limit: 1,
43
67
});
44
68
45
45
-
const handle = actorsResponse.actors.length > 0
46
46
-
? actorsResponse.actors[0].handle ?? did
47
47
-
: did; // fallback to DID if no handle found
69
69
+
const handle =
70
70
+
actorsResponse.actors.length > 0
71
71
+
? actorsResponse.actors[0].handle ?? did
72
72
+
: did; // fallback to DID if no handle found
48
73
49
74
return actorToView(profileRecord, handle);
50
75
} catch (error) {
···
56
81
export function sliceToView(
57
82
sliceRecord: RecordResponse<NetworkSlicesSlice>,
58
83
creator: NetworkSlicesActorDefsProfileViewBasic,
84
84
+
sparkline?: NetworkSlicesSliceDefsSparklinePoint[]
59
85
): NetworkSlicesSliceDefsSliceView {
60
86
return {
61
87
uri: sliceRecord.uri,
···
64
90
domain: sliceRecord.value.domain,
65
91
creator,
66
92
createdAt: sliceRecord.value.createdAt,
93
93
+
sparkline,
67
94
};
68
95
}
69
96
70
97
export function actorToView(
71
98
profileRecord: RecordResponse<NetworkSlicesActorProfile>,
72
72
-
handle: string,
99
99
+
handle: string
73
100
): NetworkSlicesActorDefsProfileViewBasic {
74
101
return {
75
102
did: profileRecord.did,
···
84
111
85
112
export async function getSlicesForActor(
86
113
client: AtProtoClient,
87
87
-
did: string,
114
114
+
did: string
88
115
): Promise<NetworkSlicesSliceDefsSliceView[]> {
89
116
try {
90
117
const slicesResponse = await client.network.slices.slice.getRecords({
91
118
where: {
92
119
did: { eq: did },
93
120
},
121
121
+
sortBy: [{ field: "createdAt", direction: "desc" }],
94
122
});
95
123
124
124
+
// Collect slice URIs for batch sparkline fetch
125
125
+
const sliceUris = slicesResponse.records.map((record) => record.uri);
126
126
+
127
127
+
// Fetch sparklines for all slices at once
128
128
+
const sparklinesMap = await fetchSparklinesForSlices(client, sliceUris);
129
129
+
96
130
const sliceViews: NetworkSlicesSliceDefsSliceView[] = [];
97
131
for (const sliceRecord of slicesResponse.records) {
98
132
const creator = await getSliceActor(client, sliceRecord.did);
99
133
if (creator) {
100
100
-
sliceViews.push(sliceToView(sliceRecord, creator));
134
134
+
const sparklineData = sparklinesMap[sliceRecord.uri];
135
135
+
sliceViews.push(sliceToView(sliceRecord, creator, sparklineData));
101
136
}
102
137
}
103
138
···
111
146
export async function searchSlices(
112
147
client: AtProtoClient,
113
148
query: string,
114
114
-
limit = 20,
149
149
+
limit = 20
115
150
): Promise<NetworkSlicesSliceDefsSliceView[]> {
116
151
try {
117
152
const slicesResponse = await client.network.slices.slice.getRecords({
···
121
156
limit,
122
157
});
123
158
159
159
+
// Collect slice URIs for batch sparkline fetch
160
160
+
const sliceUris = slicesResponse.records.map((record) => record.uri);
161
161
+
162
162
+
// Fetch sparklines for all slices at once
163
163
+
const sparklinesMap = await fetchSparklinesForSlices(client, sliceUris);
164
164
+
124
165
const sliceViews: NetworkSlicesSliceDefsSliceView[] = [];
125
166
for (const sliceRecord of slicesResponse.records) {
126
167
const creator = await getSliceActor(client, sliceRecord.did);
127
168
if (creator) {
128
128
-
sliceViews.push(sliceToView(sliceRecord, creator));
169
169
+
const sparklineData = sparklinesMap[sliceRecord.uri];
170
170
+
sliceViews.push(sliceToView(sliceRecord, creator, sparklineData));
129
171
}
130
172
}
131
173
···
138
180
139
181
export async function getTimeline(
140
182
client: AtProtoClient,
141
141
-
limit = 50,
183
183
+
limit = 50
142
184
): Promise<NetworkSlicesSliceDefsSliceView[]> {
143
185
try {
144
186
const slicesResponse = await client.network.slices.slice.getRecords({
···
146
188
sortBy: [{ field: "createdAt", direction: "desc" }],
147
189
});
148
190
191
191
+
// Collect slice URIs for batch sparkline fetch
192
192
+
const sliceUris = slicesResponse.records.map((record) => record.uri);
193
193
+
194
194
+
// Fetch sparklines for all slices at once
195
195
+
const sparklinesMap = await fetchSparklinesForSlices(client, sliceUris);
196
196
+
149
197
const sliceViews: NetworkSlicesSliceDefsSliceView[] = [];
150
198
for (const sliceRecord of slicesResponse.records) {
151
199
const creator = await getSliceActor(client, sliceRecord.did);
152
200
if (creator) {
153
153
-
sliceViews.push(sliceToView(sliceRecord, creator));
201
201
+
const sparklineData = sparklinesMap[sliceRecord.uri];
202
202
+
sliceViews.push(sliceToView(sliceRecord, creator, sparklineData));
154
203
}
155
204
}
156
205
+18
-25
frontend/src/shared/fragments/ActivitySparkline.tsx
···
1
1
+
import type { NetworkSlicesSliceDefsSparklinePoint } from "../../client.ts";
2
2
+
1
3
interface ActivitySparklineProps {
2
2
-
data?: number[];
4
4
+
sparklineData?: NetworkSlicesSliceDefsSparklinePoint[];
3
5
width?: number;
4
6
height?: number;
5
7
className?: string;
6
8
}
7
9
8
10
export function ActivitySparkline({
9
9
-
data = [],
11
11
+
sparklineData,
10
12
width = 100,
11
13
height = 30,
12
14
className = "",
13
15
}: ActivitySparklineProps) {
14
14
-
// Generate mock data if none provided (for demo purposes)
15
15
-
const sparklineData = data.length > 0 ? data : generateMockData();
16
16
+
// Convert sparkline data to numbers, or create flat line for no data
17
17
+
let dataPoints: number[];
18
18
+
if (!sparklineData || sparklineData.length === 0) {
19
19
+
// Create a flat line with 24 zero points (24 hours)
20
20
+
dataPoints = Array(24).fill(0);
21
21
+
} else {
22
22
+
dataPoints = sparklineData.map(point => point.count);
23
23
+
}
16
24
17
25
// Calculate the path for the sparkline
18
18
-
const max = Math.max(...sparklineData, 1);
19
19
-
const min = Math.min(...sparklineData, 0);
26
26
+
const max = Math.max(...dataPoints, 1);
27
27
+
const min = Math.min(...dataPoints, 0);
20
28
const range = max - min || 1;
21
29
22
30
// Create SVG path points
23
23
-
const points = sparklineData.map((value, index) => {
24
24
-
const x = (index / (sparklineData.length - 1)) * width;
31
31
+
const points = dataPoints.map((value, index) => {
32
32
+
const x = (index / (dataPoints.length - 1)) * width;
25
33
const y = height - ((value - min) / range) * height;
26
34
return `${x},${y}`;
27
35
}).join(" ");
···
78
86
<circle
79
87
cx={width}
80
88
cy={height -
81
81
-
((sparklineData[sparklineData.length - 1] - min) / range) * height}
89
89
+
((dataPoints[dataPoints.length - 1] - min) / range) * height}
82
90
r="2"
83
91
fill="#3b82f6"
84
92
/>
···
86
94
87
95
{/* Activity label */}
88
96
<div className="ml-2 text-xs text-zinc-500">
89
89
-
{sparklineData[sparklineData.length - 1]} ops/min
97
97
+
{dataPoints[dataPoints.length - 1]} records
90
98
</div>
91
99
</div>
92
100
);
93
101
}
94
102
95
95
-
// Generate mock data for demonstration
96
96
-
function generateMockData(): number[] {
97
97
-
const points = 20;
98
98
-
const data: number[] = [];
99
99
-
let lastValue = Math.random() * 50 + 10;
100
100
-
101
101
-
for (let i = 0; i < points; i++) {
102
102
-
// Random walk with some smoothing
103
103
-
const change = (Math.random() - 0.5) * 20;
104
104
-
lastValue = Math.max(0, Math.min(100, lastValue + change));
105
105
-
data.push(Math.round(lastValue));
106
106
-
}
107
107
-
108
108
-
return data;
109
109
-
}
+3
-5
frontend/src/shared/fragments/SliceCard.tsx
···
44
44
</div>
45
45
46
46
{/* Right side - activity sparkline */}
47
47
-
{
48
48
-
/* <div className="flex items-center ml-4 self-center">
49
49
-
<ActivitySparkline />
50
50
-
</div> */
51
51
-
}
47
47
+
<div className="flex items-center ml-4 self-center">
48
48
+
<ActivitySparkline sparklineData={slice.sparkline} />
49
49
+
</div>
52
50
</div>
53
51
</div>
54
52
</a>
+22
lexicons/network/slices/slice/defs.json
···
30
30
"createdAt": {
31
31
"type": "string",
32
32
"format": "datetime"
33
33
+
},
34
34
+
"sparkline": {
35
35
+
"type": "array",
36
36
+
"items": {
37
37
+
"type": "ref",
38
38
+
"ref": "#sparklinePoint"
39
39
+
},
40
40
+
"description": "Recent activity sparkline data points for the last 24 hours"
41
41
+
}
42
42
+
}
43
43
+
},
44
44
+
"sparklinePoint": {
45
45
+
"type": "object",
46
46
+
"required": ["timestamp", "count"],
47
47
+
"properties": {
48
48
+
"timestamp": {
49
49
+
"type": "string",
50
50
+
"format": "datetime"
51
51
+
},
52
52
+
"count": {
53
53
+
"type": "integer",
54
54
+
"minimum": 0
33
55
}
34
56
}
35
57
}