@serve.zone/catalog 2.2.0 → 2.3.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.
@@ -0,0 +1,667 @@
1
+ import {
2
+ DeesElement,
3
+ customElement,
4
+ html,
5
+ css,
6
+ cssManager,
7
+ property,
8
+ type TemplateResult,
9
+ } from '@design.estate/dees-element';
10
+
11
+ declare global {
12
+ interface HTMLElementTagNameMap {
13
+ 'sz-route-card': SzRouteCard;
14
+ }
15
+ }
16
+
17
+ // Simplified route types for display purposes
18
+ export type TRouteActionType = 'forward' | 'socket-handler';
19
+ export type TTlsMode = 'passthrough' | 'terminate' | 'terminate-and-reencrypt';
20
+ export type TPortRange = number | number[] | Array<{ from: number; to: number }>;
21
+
22
+ export interface IRouteMatch {
23
+ ports: TPortRange;
24
+ domains?: string | string[];
25
+ path?: string;
26
+ clientIp?: string[];
27
+ tlsVersion?: string[];
28
+ headers?: Record<string, string>;
29
+ protocol?: 'http' | 'tcp';
30
+ }
31
+
32
+ export interface IRouteTarget {
33
+ host: string | string[];
34
+ port: number | 'preserve';
35
+ }
36
+
37
+ export interface IRouteTls {
38
+ mode: TTlsMode;
39
+ certificate?: 'auto' | { key: string; cert: string };
40
+ }
41
+
42
+ export interface IRouteAction {
43
+ type: TRouteActionType;
44
+ targets?: IRouteTarget[];
45
+ tls?: IRouteTls;
46
+ websocket?: { enabled: boolean };
47
+ loadBalancing?: { algorithm: 'round-robin' | 'least-connections' | 'ip-hash' };
48
+ forwardingEngine?: 'node' | 'nftables';
49
+ }
50
+
51
+ export interface IRouteSecurity {
52
+ ipAllowList?: string[];
53
+ ipBlockList?: string[];
54
+ maxConnections?: number;
55
+ rateLimit?: { enabled: boolean; maxRequests: number; window: number };
56
+ }
57
+
58
+ export interface IRouteConfig {
59
+ id?: string;
60
+ match: IRouteMatch;
61
+ action: IRouteAction;
62
+ security?: IRouteSecurity;
63
+ headers?: { request?: Record<string, string>; response?: Record<string, string> };
64
+ name?: string;
65
+ description?: string;
66
+ priority?: number;
67
+ tags?: string[];
68
+ enabled?: boolean;
69
+ }
70
+
71
+ function formatPorts(ports: TPortRange): string {
72
+ if (typeof ports === 'number') return String(ports);
73
+ if (Array.isArray(ports)) {
74
+ return ports
75
+ .map((p) => {
76
+ if (typeof p === 'number') return String(p);
77
+ return `${p.from}\u2013${p.to}`;
78
+ })
79
+ .join(', ');
80
+ }
81
+ return String(ports);
82
+ }
83
+
84
+ function formatTargets(targets: IRouteTarget[]): string[] {
85
+ const result: string[] = [];
86
+ for (const t of targets) {
87
+ const hosts = Array.isArray(t.host) ? t.host : [t.host];
88
+ const portStr = t.port === 'preserve' ? '(preserve)' : String(t.port);
89
+ for (const h of hosts) {
90
+ result.push(`${h}:${portStr}`);
91
+ }
92
+ }
93
+ return result;
94
+ }
95
+
96
+ @customElement('sz-route-card')
97
+ export class SzRouteCard extends DeesElement {
98
+ public static demo = () => html`
99
+ <div style="padding: 24px; max-width: 520px;">
100
+ <sz-route-card
101
+ .route=${{
102
+ name: 'API Gateway',
103
+ description: 'Main API gateway with TLS termination and load balancing',
104
+ enabled: true,
105
+ priority: 10,
106
+ tags: ['web', 'api', 'production'],
107
+ match: {
108
+ ports: [443, 8443],
109
+ domains: ['api.example.com', '*.api.serve.zone'],
110
+ path: '/api/*',
111
+ protocol: 'http' as const,
112
+ clientIp: ['10.0.0.0/8'],
113
+ },
114
+ action: {
115
+ type: 'forward' as const,
116
+ targets: [
117
+ { host: ['10.0.0.1', '10.0.0.2'], port: 8080 },
118
+ ],
119
+ tls: { mode: 'terminate' as const, certificate: 'auto' as const },
120
+ websocket: { enabled: true },
121
+ loadBalancing: { algorithm: 'round-robin' as const },
122
+ forwardingEngine: 'nftables' as const,
123
+ },
124
+ security: {
125
+ ipAllowList: ['10.0.0.0/8'],
126
+ ipBlockList: ['192.168.100.0/24'],
127
+ rateLimit: { enabled: true, maxRequests: 100, window: 60 },
128
+ maxConnections: 1000,
129
+ },
130
+ } satisfies IRouteConfig}
131
+ ></sz-route-card>
132
+ </div>
133
+ `;
134
+
135
+ public static demoGroups = ['Routes'];
136
+
137
+ @property({ type: Object })
138
+ public accessor route: IRouteConfig | null = null;
139
+
140
+ public static styles = [
141
+ cssManager.defaultStyles,
142
+ css`
143
+ :host {
144
+ display: block;
145
+ }
146
+
147
+ .card {
148
+ background: ${cssManager.bdTheme('#ffffff', '#09090b')};
149
+ border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
150
+ border-radius: 8px;
151
+ padding: 20px;
152
+ transition: border-color 200ms ease, box-shadow 200ms ease;
153
+ }
154
+
155
+ .card:hover {
156
+ border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
157
+ box-shadow: 0 2px 8px ${cssManager.bdTheme('rgba(0,0,0,0.06)', 'rgba(0,0,0,0.2)')};
158
+ }
159
+
160
+ /* Header */
161
+ .header {
162
+ display: flex;
163
+ align-items: flex-start;
164
+ justify-content: space-between;
165
+ gap: 12px;
166
+ margin-bottom: 4px;
167
+ }
168
+
169
+ .header-left {
170
+ display: flex;
171
+ align-items: center;
172
+ gap: 8px;
173
+ min-width: 0;
174
+ }
175
+
176
+ .status-dot {
177
+ width: 8px;
178
+ height: 8px;
179
+ border-radius: 50%;
180
+ flex-shrink: 0;
181
+ }
182
+
183
+ .status-dot.enabled {
184
+ background: ${cssManager.bdTheme('#22c55e', '#22c55e')};
185
+ box-shadow: 0 0 6px ${cssManager.bdTheme('rgba(34,197,94,0.4)', 'rgba(34,197,94,0.3)')};
186
+ }
187
+
188
+ .status-dot.disabled {
189
+ background: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
190
+ }
191
+
192
+ .route-name {
193
+ font-size: 15px;
194
+ font-weight: 600;
195
+ color: ${cssManager.bdTheme('#18181b', '#fafafa')};
196
+ white-space: nowrap;
197
+ overflow: hidden;
198
+ text-overflow: ellipsis;
199
+ }
200
+
201
+ .header-badges {
202
+ display: flex;
203
+ gap: 6px;
204
+ flex-shrink: 0;
205
+ }
206
+
207
+ .badge {
208
+ display: inline-flex;
209
+ align-items: center;
210
+ padding: 2px 8px;
211
+ border-radius: 9999px;
212
+ font-size: 11px;
213
+ font-weight: 500;
214
+ white-space: nowrap;
215
+ }
216
+
217
+ .badge.forward {
218
+ background: ${cssManager.bdTheme('#dbeafe', 'rgba(59, 130, 246, 0.2)')};
219
+ color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
220
+ }
221
+
222
+ .badge.socket-handler {
223
+ background: ${cssManager.bdTheme('#ede9fe', 'rgba(139, 92, 246, 0.2)')};
224
+ color: ${cssManager.bdTheme('#7c3aed', '#a78bfa')};
225
+ }
226
+
227
+ .badge.enabled {
228
+ background: ${cssManager.bdTheme('#dcfce7', 'rgba(34, 197, 94, 0.2)')};
229
+ color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
230
+ }
231
+
232
+ .badge.disabled {
233
+ background: ${cssManager.bdTheme('#f4f4f5', 'rgba(113, 113, 122, 0.2)')};
234
+ color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
235
+ }
236
+
237
+ .description {
238
+ font-size: 13px;
239
+ color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
240
+ margin-bottom: 8px;
241
+ line-height: 1.4;
242
+ }
243
+
244
+ .meta-row {
245
+ display: flex;
246
+ align-items: center;
247
+ justify-content: space-between;
248
+ flex-wrap: wrap;
249
+ gap: 6px;
250
+ margin-bottom: 16px;
251
+ }
252
+
253
+ .tags {
254
+ display: flex;
255
+ flex-wrap: wrap;
256
+ gap: 4px;
257
+ }
258
+
259
+ .tag {
260
+ padding: 2px 8px;
261
+ background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
262
+ border-radius: 4px;
263
+ font-size: 11px;
264
+ font-weight: 500;
265
+ color: ${cssManager.bdTheme('#52525b', '#a1a1aa')};
266
+ }
267
+
268
+ .priority {
269
+ font-size: 11px;
270
+ color: ${cssManager.bdTheme('#a1a1aa', '#71717a')};
271
+ font-weight: 500;
272
+ }
273
+
274
+ /* Sections */
275
+ .section {
276
+ border-left: 3px solid;
277
+ padding: 10px 14px;
278
+ margin-bottom: 12px;
279
+ border-radius: 0 6px 6px 0;
280
+ background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
281
+ }
282
+
283
+ .section:last-of-type {
284
+ margin-bottom: 0;
285
+ }
286
+
287
+ .section.match {
288
+ border-left-color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
289
+ }
290
+
291
+ .section.action {
292
+ border-left-color: ${cssManager.bdTheme('#22c55e', '#22c55e')};
293
+ }
294
+
295
+ .section.security {
296
+ border-left-color: ${cssManager.bdTheme('#f59e0b', '#f59e0b')};
297
+ }
298
+
299
+ .section-label {
300
+ font-size: 10px;
301
+ font-weight: 600;
302
+ text-transform: uppercase;
303
+ letter-spacing: 0.08em;
304
+ color: ${cssManager.bdTheme('#a1a1aa', '#71717a')};
305
+ margin-bottom: 8px;
306
+ }
307
+
308
+ .field-row {
309
+ display: flex;
310
+ gap: 8px;
311
+ margin-bottom: 5px;
312
+ font-size: 13px;
313
+ line-height: 1.5;
314
+ }
315
+
316
+ .field-row:last-child {
317
+ margin-bottom: 0;
318
+ }
319
+
320
+ .field-key {
321
+ color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
322
+ min-width: 64px;
323
+ flex-shrink: 0;
324
+ font-weight: 500;
325
+ }
326
+
327
+ .field-value {
328
+ color: ${cssManager.bdTheme('#18181b', '#e4e4e7')};
329
+ word-break: break-all;
330
+ }
331
+
332
+ .domain-chip {
333
+ display: inline-flex;
334
+ padding: 1px 6px;
335
+ background: ${cssManager.bdTheme('#eff6ff', 'rgba(59, 130, 246, 0.1)')};
336
+ border-radius: 3px;
337
+ font-size: 12px;
338
+ margin-right: 4px;
339
+ margin-bottom: 2px;
340
+ font-family: monospace;
341
+ }
342
+
343
+ .domain-chip.glob {
344
+ background: ${cssManager.bdTheme('#fef3c7', 'rgba(245, 158, 11, 0.15)')};
345
+ color: ${cssManager.bdTheme('#92400e', '#fbbf24')};
346
+ }
347
+
348
+ .mono {
349
+ font-family: monospace;
350
+ font-size: 12px;
351
+ }
352
+
353
+ .protocol-badge {
354
+ display: inline-flex;
355
+ padding: 1px 6px;
356
+ border-radius: 3px;
357
+ font-size: 11px;
358
+ font-weight: 500;
359
+ }
360
+
361
+ .protocol-badge.http {
362
+ background: ${cssManager.bdTheme('#dbeafe', 'rgba(59, 130, 246, 0.2)')};
363
+ color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
364
+ }
365
+
366
+ .protocol-badge.tcp {
367
+ background: ${cssManager.bdTheme('#dcfce7', 'rgba(34, 197, 94, 0.2)')};
368
+ color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
369
+ }
370
+
371
+ .tls-badge {
372
+ display: inline-flex;
373
+ padding: 1px 6px;
374
+ border-radius: 3px;
375
+ font-size: 11px;
376
+ font-weight: 500;
377
+ }
378
+
379
+ .tls-badge.auto {
380
+ background: ${cssManager.bdTheme('#dcfce7', 'rgba(34, 197, 94, 0.2)')};
381
+ color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
382
+ }
383
+
384
+ .tls-badge.custom {
385
+ background: ${cssManager.bdTheme('#ffedd5', 'rgba(249, 115, 22, 0.2)')};
386
+ color: ${cssManager.bdTheme('#c2410c', '#fb923c')};
387
+ }
388
+
389
+ .engine-badge {
390
+ display: inline-flex;
391
+ padding: 1px 6px;
392
+ border-radius: 3px;
393
+ font-size: 11px;
394
+ font-weight: 500;
395
+ background: ${cssManager.bdTheme('#fae8ff', 'rgba(168, 85, 247, 0.2)')};
396
+ color: ${cssManager.bdTheme('#7e22ce', '#c084fc')};
397
+ }
398
+
399
+ .header-pair {
400
+ display: inline;
401
+ font-family: monospace;
402
+ font-size: 12px;
403
+ }
404
+
405
+ /* Feature icons */
406
+ .features-row {
407
+ display: flex;
408
+ flex-wrap: wrap;
409
+ gap: 10px;
410
+ margin-top: 14px;
411
+ padding-top: 12px;
412
+ border-top: 1px solid ${cssManager.bdTheme('#f4f4f5', '#1a1a1a')};
413
+ }
414
+
415
+ .feature {
416
+ display: flex;
417
+ align-items: center;
418
+ gap: 4px;
419
+ font-size: 11px;
420
+ font-weight: 500;
421
+ color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
422
+ }
423
+
424
+ .feature-icon {
425
+ font-size: 13px;
426
+ }
427
+
428
+ .no-route {
429
+ text-align: center;
430
+ padding: 24px;
431
+ color: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
432
+ font-size: 13px;
433
+ }
434
+ `,
435
+ ];
436
+
437
+ public render(): TemplateResult {
438
+ if (!this.route) {
439
+ return html`<div class="card"><div class="no-route">No route data</div></div>`;
440
+ }
441
+
442
+ const r = this.route;
443
+ const isEnabled = r.enabled !== false;
444
+ const match = r.match;
445
+ const action = r.action;
446
+ const security = r.security;
447
+
448
+ return html`
449
+ <div class="card">
450
+ <!-- Header -->
451
+ <div class="header">
452
+ <div class="header-left">
453
+ <span class="status-dot ${isEnabled ? 'enabled' : 'disabled'}"></span>
454
+ <span class="route-name">${r.name || r.id || 'Unnamed Route'}</span>
455
+ </div>
456
+ <div class="header-badges">
457
+ <span class="badge ${action.type}">${action.type}</span>
458
+ <span class="badge ${isEnabled ? 'enabled' : 'disabled'}">${isEnabled ? 'enabled' : 'disabled'}</span>
459
+ </div>
460
+ </div>
461
+
462
+ ${r.description ? html`<div class="description">${r.description}</div>` : ''}
463
+
464
+ <div class="meta-row">
465
+ ${r.tags && r.tags.length > 0
466
+ ? html`<div class="tags">${r.tags.map((t) => html`<span class="tag">${t}</span>`)}</div>`
467
+ : html`<div></div>`}
468
+ ${r.priority != null ? html`<span class="priority">Priority: ${r.priority}</span>` : ''}
469
+ </div>
470
+
471
+ <!-- Match Section -->
472
+ <div class="section match">
473
+ <div class="section-label">Match</div>
474
+ <div class="field-row">
475
+ <span class="field-key">Ports</span>
476
+ <span class="field-value mono">${formatPorts(match.ports)}</span>
477
+ </div>
478
+ ${match.domains
479
+ ? html`
480
+ <div class="field-row">
481
+ <span class="field-key">Domains</span>
482
+ <span class="field-value">${this.renderDomains(match.domains)}</span>
483
+ </div>
484
+ `
485
+ : ''}
486
+ ${match.path
487
+ ? html`
488
+ <div class="field-row">
489
+ <span class="field-key">Path</span>
490
+ <span class="field-value mono">${match.path}</span>
491
+ </div>
492
+ `
493
+ : ''}
494
+ ${match.protocol
495
+ ? html`
496
+ <div class="field-row">
497
+ <span class="field-key">Protocol</span>
498
+ <span class="field-value">
499
+ <span class="protocol-badge ${match.protocol}">${match.protocol}</span>
500
+ </span>
501
+ </div>
502
+ `
503
+ : ''}
504
+ ${match.clientIp && match.clientIp.length > 0
505
+ ? html`
506
+ <div class="field-row">
507
+ <span class="field-key">Client</span>
508
+ <span class="field-value mono">${match.clientIp.join(', ')}</span>
509
+ </div>
510
+ `
511
+ : ''}
512
+ ${match.tlsVersion && match.tlsVersion.length > 0
513
+ ? html`
514
+ <div class="field-row">
515
+ <span class="field-key">TLS Ver</span>
516
+ <span class="field-value">${match.tlsVersion.join(', ')}</span>
517
+ </div>
518
+ `
519
+ : ''}
520
+ ${match.headers
521
+ ? html`
522
+ <div class="field-row">
523
+ <span class="field-key">Headers</span>
524
+ <span class="field-value">
525
+ ${Object.entries(match.headers).map(
526
+ ([k, v]) => html`<span class="header-pair">${k}=${v}</span> `
527
+ )}
528
+ </span>
529
+ </div>
530
+ `
531
+ : ''}
532
+ </div>
533
+
534
+ <!-- Action Section -->
535
+ <div class="section action">
536
+ <div class="section-label">Action</div>
537
+ ${action.targets && action.targets.length > 0
538
+ ? html`
539
+ <div class="field-row">
540
+ <span class="field-key">Targets</span>
541
+ <span class="field-value mono">${formatTargets(action.targets).join(', ')}</span>
542
+ </div>
543
+ `
544
+ : ''}
545
+ ${action.tls
546
+ ? html`
547
+ <div class="field-row">
548
+ <span class="field-key">TLS</span>
549
+ <span class="field-value">
550
+ ${action.tls.mode}
551
+ ${action.tls.certificate
552
+ ? action.tls.certificate === 'auto'
553
+ ? html` <span class="tls-badge auto">auto cert</span>`
554
+ : html` <span class="tls-badge custom">custom cert</span>`
555
+ : ''}
556
+ </span>
557
+ </div>
558
+ `
559
+ : ''}
560
+ ${action.forwardingEngine
561
+ ? html`
562
+ <div class="field-row">
563
+ <span class="field-key">Engine</span>
564
+ <span class="field-value"><span class="engine-badge">${action.forwardingEngine}</span></span>
565
+ </div>
566
+ `
567
+ : ''}
568
+ ${action.loadBalancing
569
+ ? html`
570
+ <div class="field-row">
571
+ <span class="field-key">LB</span>
572
+ <span class="field-value">${action.loadBalancing.algorithm}</span>
573
+ </div>
574
+ `
575
+ : ''}
576
+ ${action.websocket?.enabled
577
+ ? html`
578
+ <div class="field-row">
579
+ <span class="field-key">WS</span>
580
+ <span class="field-value"><span class="badge enabled">enabled</span></span>
581
+ </div>
582
+ `
583
+ : ''}
584
+ </div>
585
+
586
+ <!-- Security Section -->
587
+ ${security
588
+ ? html`
589
+ <div class="section security">
590
+ <div class="section-label">Security</div>
591
+ ${security.ipAllowList && security.ipAllowList.length > 0
592
+ ? html`
593
+ <div class="field-row">
594
+ <span class="field-key">Allow</span>
595
+ <span class="field-value mono">${security.ipAllowList.join(', ')}</span>
596
+ </div>
597
+ `
598
+ : ''}
599
+ ${security.ipBlockList && security.ipBlockList.length > 0
600
+ ? html`
601
+ <div class="field-row">
602
+ <span class="field-key">Block</span>
603
+ <span class="field-value mono">${security.ipBlockList.join(', ')}</span>
604
+ </div>
605
+ `
606
+ : ''}
607
+ ${security.rateLimit?.enabled
608
+ ? html`
609
+ <div class="field-row">
610
+ <span class="field-key">Rate</span>
611
+ <span class="field-value">${security.rateLimit.maxRequests} req / ${security.rateLimit.window}s</span>
612
+ </div>
613
+ `
614
+ : ''}
615
+ ${security.maxConnections
616
+ ? html`
617
+ <div class="field-row">
618
+ <span class="field-key">Max Conn</span>
619
+ <span class="field-value">${security.maxConnections}</span>
620
+ </div>
621
+ `
622
+ : ''}
623
+ </div>
624
+ `
625
+ : ''}
626
+
627
+ <!-- Feature Icons Row -->
628
+ ${this.renderFeatures()}
629
+ </div>
630
+ `;
631
+ }
632
+
633
+ private renderDomains(domains: string | string[]): TemplateResult {
634
+ const list = Array.isArray(domains) ? domains : [domains];
635
+ return html`${list.map(
636
+ (d) =>
637
+ html`<span class="domain-chip ${d.includes('*') ? 'glob' : ''}">${d}</span>`
638
+ )}`;
639
+ }
640
+
641
+ private renderFeatures(): TemplateResult {
642
+ if (!this.route) return html``;
643
+ const features: TemplateResult[] = [];
644
+ const action = this.route.action;
645
+ const security = this.route.security;
646
+ const headers = this.route.headers;
647
+
648
+ if (action.tls) {
649
+ features.push(html`<span class="feature"><span class="feature-icon">&#x1f512;</span>TLS</span>`);
650
+ }
651
+ if (action.websocket?.enabled) {
652
+ features.push(html`<span class="feature"><span class="feature-icon">&#x2194;</span>WS</span>`);
653
+ }
654
+ if (action.loadBalancing) {
655
+ features.push(html`<span class="feature"><span class="feature-icon">&#x2696;</span>LB</span>`);
656
+ }
657
+ if (security) {
658
+ features.push(html`<span class="feature"><span class="feature-icon">&#x1f6e1;</span>Security</span>`);
659
+ }
660
+ if (headers) {
661
+ features.push(html`<span class="feature"><span class="feature-icon">&#x2699;</span>Headers</span>`);
662
+ }
663
+
664
+ if (features.length === 0) return html``;
665
+ return html`<div class="features-row">${features}</div>`;
666
+ }
667
+ }