@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,326 @@
1
+ import {
2
+ DeesElement,
3
+ customElement,
4
+ html,
5
+ css,
6
+ cssManager,
7
+ property,
8
+ state,
9
+ type TemplateResult,
10
+ } from '@design.estate/dees-element';
11
+
12
+ import type { IRouteConfig, TRouteActionType } from './sz-route-card.js';
13
+
14
+ declare global {
15
+ interface HTMLElementTagNameMap {
16
+ 'sz-route-list-view': SzRouteListView;
17
+ }
18
+ }
19
+
20
+ @customElement('sz-route-list-view')
21
+ export class SzRouteListView extends DeesElement {
22
+ public static demo = () => html`
23
+ <div style="padding: 24px; max-width: 1200px;">
24
+ <sz-route-list-view
25
+ .routes=${[
26
+ {
27
+ name: 'HTTPS Gateway',
28
+ description: 'Main web gateway with TLS termination',
29
+ enabled: true,
30
+ tags: ['web', 'https', 'production'],
31
+ match: { ports: 443, domains: ['*.example.com', 'serve.zone'], protocol: 'http' as const },
32
+ action: {
33
+ type: 'forward' as const,
34
+ targets: [{ host: '10.0.0.1', port: 8080 }],
35
+ tls: { mode: 'terminate' as const, certificate: 'auto' as const },
36
+ },
37
+ },
38
+ {
39
+ name: 'SMTP Inbound',
40
+ description: 'Email relay for incoming mail',
41
+ enabled: true,
42
+ tags: ['email', 'smtp'],
43
+ match: { ports: 25, domains: 'mail.serve.zone' },
44
+ action: {
45
+ type: 'forward' as const,
46
+ targets: [{ host: '10.0.1.5', port: 25 }],
47
+ },
48
+ },
49
+ {
50
+ name: 'WebSocket API',
51
+ description: 'Real-time WebSocket connections',
52
+ enabled: true,
53
+ tags: ['web', 'api'],
54
+ match: { ports: 443, domains: 'ws.example.com', path: '/ws/*' },
55
+ action: {
56
+ type: 'forward' as const,
57
+ targets: [{ host: '10.0.0.3', port: 9090 }],
58
+ websocket: { enabled: true },
59
+ tls: { mode: 'terminate' as const, certificate: 'auto' as const },
60
+ },
61
+ },
62
+ {
63
+ name: 'Maintenance Page',
64
+ enabled: false,
65
+ tags: ['web'],
66
+ match: { ports: [80, 443], domains: 'old.example.com' },
67
+ action: { type: 'socket-handler' as const },
68
+ },
69
+ ] satisfies IRouteConfig[]}
70
+ ></sz-route-list-view>
71
+ </div>
72
+ `;
73
+
74
+ public static demoGroups = ['Routes'];
75
+
76
+ @property({ type: Array })
77
+ public accessor routes: IRouteConfig[] = [];
78
+
79
+ @state()
80
+ private accessor searchQuery: string = '';
81
+
82
+ @state()
83
+ private accessor actionFilter: TRouteActionType | 'all' = 'all';
84
+
85
+ @state()
86
+ private accessor enabledFilter: 'all' | 'enabled' | 'disabled' = 'all';
87
+
88
+ private get filteredRoutes(): IRouteConfig[] {
89
+ return this.routes.filter((route) => {
90
+ // Action type filter
91
+ if (this.actionFilter !== 'all' && route.action.type !== this.actionFilter) return false;
92
+
93
+ // Enabled/disabled filter
94
+ if (this.enabledFilter === 'enabled' && route.enabled === false) return false;
95
+ if (this.enabledFilter === 'disabled' && route.enabled !== false) return false;
96
+
97
+ // Search query
98
+ if (this.searchQuery) {
99
+ const q = this.searchQuery.toLowerCase();
100
+ return this.routeMatchesSearch(route, q);
101
+ }
102
+ return true;
103
+ });
104
+ }
105
+
106
+ private routeMatchesSearch(route: IRouteConfig, q: string): boolean {
107
+ // Name and description
108
+ if (route.name?.toLowerCase().includes(q)) return true;
109
+ if (route.description?.toLowerCase().includes(q)) return true;
110
+
111
+ // Domains
112
+ if (route.match.domains) {
113
+ const domains = Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains];
114
+ if (domains.some((d) => d.toLowerCase().includes(q))) return true;
115
+ }
116
+
117
+ // Ports
118
+ const portsStr = this.formatPortsForSearch(route.match.ports);
119
+ if (portsStr.includes(q)) return true;
120
+
121
+ // Path
122
+ if (route.match.path?.toLowerCase().includes(q)) return true;
123
+
124
+ // Client IPs
125
+ if (route.match.clientIp?.some((ip) => ip.includes(q))) return true;
126
+
127
+ // Targets
128
+ if (route.action.targets) {
129
+ for (const t of route.action.targets) {
130
+ const hosts = Array.isArray(t.host) ? t.host : [t.host];
131
+ if (hosts.some((h) => h.toLowerCase().includes(q))) return true;
132
+ }
133
+ }
134
+
135
+ // Tags
136
+ if (route.tags?.some((t) => t.toLowerCase().includes(q))) return true;
137
+
138
+ return false;
139
+ }
140
+
141
+ private formatPortsForSearch(ports: import('./sz-route-card.js').TPortRange): string {
142
+ if (typeof ports === 'number') return String(ports);
143
+ if (Array.isArray(ports)) {
144
+ return ports
145
+ .map((p) => (typeof p === 'number' ? String(p) : `${p.from}-${p.to}`))
146
+ .join(' ');
147
+ }
148
+ return String(ports);
149
+ }
150
+
151
+ public static styles = [
152
+ cssManager.defaultStyles,
153
+ css`
154
+ :host {
155
+ display: block;
156
+ }
157
+
158
+ .filter-bar {
159
+ display: flex;
160
+ flex-wrap: wrap;
161
+ gap: 12px;
162
+ align-items: center;
163
+ margin-bottom: 12px;
164
+ }
165
+
166
+ .search-input {
167
+ flex: 1;
168
+ min-width: 200px;
169
+ padding: 8px 12px;
170
+ background: ${cssManager.bdTheme('#ffffff', '#09090b')};
171
+ border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
172
+ border-radius: 6px;
173
+ font-size: 14px;
174
+ color: ${cssManager.bdTheme('#18181b', '#fafafa')};
175
+ outline: none;
176
+ transition: border-color 200ms ease;
177
+ }
178
+
179
+ .search-input::placeholder {
180
+ color: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
181
+ }
182
+
183
+ .search-input:focus {
184
+ border-color: ${cssManager.bdTheme('#2563eb', '#3b82f6')};
185
+ }
186
+
187
+ .chip-group {
188
+ display: flex;
189
+ gap: 4px;
190
+ }
191
+
192
+ .chip {
193
+ padding: 6px 12px;
194
+ background: transparent;
195
+ border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
196
+ border-radius: 9999px;
197
+ font-size: 12px;
198
+ font-weight: 500;
199
+ color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
200
+ cursor: pointer;
201
+ transition: all 200ms ease;
202
+ white-space: nowrap;
203
+ }
204
+
205
+ .chip:hover {
206
+ background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
207
+ color: ${cssManager.bdTheme('#18181b', '#fafafa')};
208
+ }
209
+
210
+ .chip.active {
211
+ background: ${cssManager.bdTheme('#18181b', '#fafafa')};
212
+ color: ${cssManager.bdTheme('#fafafa', '#18181b')};
213
+ border-color: ${cssManager.bdTheme('#18181b', '#fafafa')};
214
+ }
215
+
216
+ .results-count {
217
+ font-size: 13px;
218
+ color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
219
+ margin-bottom: 16px;
220
+ }
221
+
222
+ .grid {
223
+ display: grid;
224
+ grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
225
+ gap: 16px;
226
+ }
227
+
228
+ .grid sz-route-card {
229
+ cursor: pointer;
230
+ }
231
+
232
+ .empty-state {
233
+ text-align: center;
234
+ padding: 48px 24px;
235
+ color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
236
+ font-size: 14px;
237
+ }
238
+
239
+ .empty-state-icon {
240
+ font-size: 32px;
241
+ margin-bottom: 12px;
242
+ opacity: 0.5;
243
+ }
244
+ `,
245
+ ];
246
+
247
+ public render(): TemplateResult {
248
+ const filtered = this.filteredRoutes;
249
+
250
+ return html`
251
+ <div class="filter-bar">
252
+ <input
253
+ class="search-input"
254
+ type="text"
255
+ placeholder="Search routes by domain, IP, port, path, or tag..."
256
+ .value=${this.searchQuery}
257
+ @input=${(e: InputEvent) => {
258
+ this.searchQuery = (e.target as HTMLInputElement).value;
259
+ }}
260
+ />
261
+ <div class="chip-group">
262
+ ${(['all', 'forward', 'socket-handler'] as const).map(
263
+ (type) => html`
264
+ <button
265
+ class="chip ${this.actionFilter === type ? 'active' : ''}"
266
+ @click=${() => {
267
+ this.actionFilter = type;
268
+ }}
269
+ >
270
+ ${type === 'all' ? 'All' : type === 'forward' ? 'Forward' : 'Socket Handler'}
271
+ </button>
272
+ `
273
+ )}
274
+ </div>
275
+ <div class="chip-group">
276
+ ${(['all', 'enabled', 'disabled'] as const).map(
277
+ (status) => html`
278
+ <button
279
+ class="chip ${this.enabledFilter === status ? 'active' : ''}"
280
+ @click=${() => {
281
+ this.enabledFilter = status;
282
+ }}
283
+ >
284
+ ${status.charAt(0).toUpperCase() + status.slice(1)}
285
+ </button>
286
+ `
287
+ )}
288
+ </div>
289
+ </div>
290
+
291
+ <div class="results-count">
292
+ Showing ${filtered.length} of ${this.routes.length} routes
293
+ </div>
294
+
295
+ ${filtered.length > 0
296
+ ? html`
297
+ <div class="grid">
298
+ ${filtered.map(
299
+ (route) => html`
300
+ <sz-route-card
301
+ .route=${route}
302
+ @click=${() => this.handleRouteClick(route)}
303
+ ></sz-route-card>
304
+ `
305
+ )}
306
+ </div>
307
+ `
308
+ : html`
309
+ <div class="empty-state">
310
+ <div class="empty-state-icon">&#x1f50d;</div>
311
+ <div>No routes match your filters</div>
312
+ </div>
313
+ `}
314
+ `;
315
+ }
316
+
317
+ private handleRouteClick(route: IRouteConfig) {
318
+ this.dispatchEvent(
319
+ new CustomEvent('route-click', {
320
+ detail: route,
321
+ bubbles: true,
322
+ composed: true,
323
+ })
324
+ );
325
+ }
326
+ }
@@ -138,6 +138,12 @@ export class SzDemoAppShell extends DeesElement {
138
138
  iconName: 'lucide:Mail',
139
139
  content: 'sz-demo-view-mta',
140
140
  },
141
+ {
142
+ id: 'routes',
143
+ name: 'Routes',
144
+ iconName: 'lucide:Route',
145
+ content: 'sz-demo-view-routes',
146
+ },
141
147
  {
142
148
  id: 'settings',
143
149
  name: 'Settings',
@@ -153,7 +159,7 @@ export class SzDemoAppShell extends DeesElement {
153
159
  },
154
160
  {
155
161
  name: 'Infrastructure',
156
- views: ['services', 'network', 'registries', 'mta'],
162
+ views: ['services', 'network', 'registries', 'mta', 'routes'],
157
163
  },
158
164
  {
159
165
  name: 'Administration',