@signe/room 2.9.4 → 3.0.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/CHANGELOG.md +13 -0
- package/dist/chunk-EUXUH3YW.js +15 -0
- package/dist/chunk-EUXUH3YW.js.map +1 -0
- package/dist/cloudflare/index.d.ts +71 -0
- package/dist/cloudflare/index.js +320 -0
- package/dist/cloudflare/index.js.map +1 -0
- package/dist/index.d.ts +65 -188
- package/dist/index.js +742 -146
- package/dist/index.js.map +1 -1
- package/dist/node/index.d.ts +164 -0
- package/dist/node/index.js +786 -0
- package/dist/node/index.js.map +1 -0
- package/dist/party-dNs-hqkq.d.ts +175 -0
- package/examples/cloudflare/README.md +62 -0
- package/examples/cloudflare/node_modules/.bin/tsc +17 -0
- package/examples/cloudflare/node_modules/.bin/tsserver +17 -0
- package/examples/cloudflare/node_modules/.bin/wrangler +17 -0
- package/examples/cloudflare/node_modules/.bin/wrangler2 +17 -0
- package/examples/cloudflare/package.json +24 -0
- package/examples/cloudflare/public/index.html +443 -0
- package/examples/cloudflare/src/index.ts +28 -0
- package/examples/cloudflare/src/room.ts +44 -0
- package/examples/cloudflare/tsconfig.json +10 -0
- package/examples/cloudflare/wrangler.jsonc +25 -0
- package/examples/node/README.md +57 -0
- package/examples/node/node_modules/.bin/tsc +17 -0
- package/examples/node/node_modules/.bin/tsserver +17 -0
- package/examples/node/node_modules/.bin/tsx +17 -0
- package/examples/node/package.json +23 -0
- package/examples/node/public/index.html +443 -0
- package/examples/node/room.ts +44 -0
- package/examples/node/server.sqlite.ts +52 -0
- package/examples/node/server.ts +51 -0
- package/examples/node/tsconfig.json +10 -0
- package/examples/node-game/README.md +66 -0
- package/examples/node-game/package.json +23 -0
- package/examples/node-game/public/index.html +705 -0
- package/examples/node-game/room.ts +145 -0
- package/examples/node-game/server.sqlite.ts +54 -0
- package/examples/node-game/server.ts +53 -0
- package/examples/node-game/tsconfig.json +10 -0
- package/examples/node-shard/README.md +32 -0
- package/examples/node-shard/dev.ts +39 -0
- package/examples/node-shard/package.json +24 -0
- package/examples/node-shard/public/index.html +777 -0
- package/examples/node-shard/room-server.ts +68 -0
- package/examples/node-shard/room.ts +105 -0
- package/examples/node-shard/shared.ts +6 -0
- package/examples/node-shard/tsconfig.json +14 -0
- package/examples/node-shard/world-server.ts +169 -0
- package/package.json +14 -5
- package/readme.md +377 -11
- package/src/cloudflare/index.ts +474 -0
- package/src/mock.ts +29 -7
- package/src/node/index.ts +1112 -0
- package/src/server.ts +626 -90
- package/src/session.guard.ts +6 -2
- package/src/shard.ts +91 -23
- package/src/storage.ts +29 -5
- package/src/testing.ts +4 -3
- package/src/types/party.ts +4 -1
- package/src/world.guard.ts +23 -4
- package/src/world.ts +170 -79
- package/examples/game/.vscode/launch.json +0 -11
- package/examples/game/.vscode/settings.json +0 -11
- package/examples/game/README.md +0 -40
- package/examples/game/app/client.tsx +0 -15
- package/examples/game/app/components/Admin.tsx +0 -1089
- package/examples/game/app/components/Room.tsx +0 -162
- package/examples/game/app/styles.css +0 -31
- package/examples/game/package-lock.json +0 -225
- package/examples/game/package.json +0 -20
- package/examples/game/party/game.room.ts +0 -32
- package/examples/game/party/server.ts +0 -10
- package/examples/game/party/shard.ts +0 -5
- package/examples/game/partykit.json +0 -14
- package/examples/game/public/favicon.ico +0 -0
- package/examples/game/public/index.html +0 -27
- package/examples/game/public/normalize.css +0 -351
- package/examples/game/shared/room.schema.ts +0 -14
- package/examples/game/tsconfig.json +0 -109
|
@@ -1,1089 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React, { useState, useEffect, useRef } from 'react';
|
|
4
|
-
import Room from './Room';
|
|
5
|
-
import { connectionRoom } from '../../../../../sync/src/client';
|
|
6
|
-
import { signal, effect } from '@signe/reactive';
|
|
7
|
-
|
|
8
|
-
// Classe client pour représenter le world côté client
|
|
9
|
-
class WorldClient {
|
|
10
|
-
rooms = signal<Record<string, any>>({});
|
|
11
|
-
shards = signal<Record<string, any>>({});
|
|
12
|
-
roomShards = signal<Record<string, string[]>>({});
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// Interfaces pour les données du World
|
|
16
|
-
interface RoomConfig {
|
|
17
|
-
name: string;
|
|
18
|
-
balancingStrategy: 'round-robin' | 'least-connections' | 'random';
|
|
19
|
-
public: boolean;
|
|
20
|
-
maxPlayersPerShard: number;
|
|
21
|
-
minShards: number;
|
|
22
|
-
maxShards?: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface ShardInfo {
|
|
26
|
-
id: string;
|
|
27
|
-
url: string;
|
|
28
|
-
connections: number;
|
|
29
|
-
capacity: number;
|
|
30
|
-
status?: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface RoomInfo {
|
|
34
|
-
roomId: string;
|
|
35
|
-
config: RoomConfig;
|
|
36
|
-
shards: ShardInfo[];
|
|
37
|
-
metrics: {
|
|
38
|
-
totalConnections: number;
|
|
39
|
-
totalCapacity: number;
|
|
40
|
-
utilizationPercentage: number;
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface WorldInfo {
|
|
45
|
-
rooms: RoomInfo[];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Composant qui affiche un loading spinner
|
|
49
|
-
const LoadingSpinner = () => (
|
|
50
|
-
<div className="loading-spinner">
|
|
51
|
-
<div className="spinner"></div>
|
|
52
|
-
{/* @ts-ignore */}
|
|
53
|
-
<style jsx>{`
|
|
54
|
-
.loading-spinner {
|
|
55
|
-
display: flex;
|
|
56
|
-
justify-content: center;
|
|
57
|
-
align-items: center;
|
|
58
|
-
height: 100px;
|
|
59
|
-
}
|
|
60
|
-
.spinner {
|
|
61
|
-
border: 4px solid rgba(0, 0, 0, 0.1);
|
|
62
|
-
width: 36px;
|
|
63
|
-
height: 36px;
|
|
64
|
-
border-radius: 50%;
|
|
65
|
-
border-left-color: #2563eb;
|
|
66
|
-
animation: spin 1s linear infinite;
|
|
67
|
-
}
|
|
68
|
-
@keyframes spin {
|
|
69
|
-
0% { transform: rotate(0deg); }
|
|
70
|
-
100% { transform: rotate(360deg); }
|
|
71
|
-
}
|
|
72
|
-
`}</style>
|
|
73
|
-
</div>
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
// Composant pour afficher une barre de progression
|
|
77
|
-
const ProgressBar = ({ percentage }: { percentage: number }) => {
|
|
78
|
-
const barColor =
|
|
79
|
-
percentage < 60 ? '#22c55e' : // vert si <60%
|
|
80
|
-
percentage < 80 ? '#f59e0b' : // jaune si <80%
|
|
81
|
-
'#ef4444'; // rouge si >=80%
|
|
82
|
-
|
|
83
|
-
return (
|
|
84
|
-
<div className="progress-container">
|
|
85
|
-
<div
|
|
86
|
-
className="progress-bar"
|
|
87
|
-
style={{ width: `${percentage}%`, backgroundColor: barColor }}
|
|
88
|
-
></div>
|
|
89
|
-
<span className="progress-label">{percentage}%</span>
|
|
90
|
-
{/* @ts-ignore */}
|
|
91
|
-
<style jsx>{`
|
|
92
|
-
.progress-container {
|
|
93
|
-
width: 100%;
|
|
94
|
-
height: 20px;
|
|
95
|
-
background-color: #e5e7eb;
|
|
96
|
-
border-radius: 10px;
|
|
97
|
-
position: relative;
|
|
98
|
-
overflow: hidden;
|
|
99
|
-
}
|
|
100
|
-
.progress-bar {
|
|
101
|
-
height: 100%;
|
|
102
|
-
border-radius: 10px;
|
|
103
|
-
transition: width 0.3s ease;
|
|
104
|
-
}
|
|
105
|
-
.progress-label {
|
|
106
|
-
position: absolute;
|
|
107
|
-
top: 50%;
|
|
108
|
-
left: 50%;
|
|
109
|
-
transform: translate(-50%, -50%);
|
|
110
|
-
color: #fff;
|
|
111
|
-
font-weight: bold;
|
|
112
|
-
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
|
|
113
|
-
}
|
|
114
|
-
`}</style>
|
|
115
|
-
</div>
|
|
116
|
-
);
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
// Composant pour une carte de salle
|
|
120
|
-
const RoomCard = ({ room, onSelect }: { room: RoomInfo; onSelect: () => void }) => {
|
|
121
|
-
return (
|
|
122
|
-
<div className="room-card" onClick={onSelect}>
|
|
123
|
-
<h3>{room.config.name} <span className="room-id">({room.roomId})</span></h3>
|
|
124
|
-
<div className="room-stats">
|
|
125
|
-
<div className="stat">
|
|
126
|
-
<span className="label">Shards:</span>
|
|
127
|
-
<span className="value">{room.shards.length}</span>
|
|
128
|
-
</div>
|
|
129
|
-
<div className="stat">
|
|
130
|
-
<span className="label">Utilisateurs:</span>
|
|
131
|
-
<span className="value">{room.metrics.totalConnections} / {room.metrics.totalCapacity}</span>
|
|
132
|
-
</div>
|
|
133
|
-
<div className="stat">
|
|
134
|
-
<span className="label">Utilisation:</span>
|
|
135
|
-
</div>
|
|
136
|
-
<ProgressBar percentage={room.metrics.utilizationPercentage} />
|
|
137
|
-
</div>
|
|
138
|
-
{/* @ts-ignore */}
|
|
139
|
-
<style jsx>{`
|
|
140
|
-
.room-card {
|
|
141
|
-
background: white;
|
|
142
|
-
border-radius: 8px;
|
|
143
|
-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
144
|
-
padding: 16px;
|
|
145
|
-
margin-bottom: 16px;
|
|
146
|
-
cursor: pointer;
|
|
147
|
-
transition: all 0.2s;
|
|
148
|
-
}
|
|
149
|
-
.room-card:hover {
|
|
150
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
151
|
-
transform: translateY(-2px);
|
|
152
|
-
}
|
|
153
|
-
.room-id {
|
|
154
|
-
color: #6b7280;
|
|
155
|
-
font-size: 0.9em;
|
|
156
|
-
}
|
|
157
|
-
.room-stats {
|
|
158
|
-
margin-top: 12px;
|
|
159
|
-
}
|
|
160
|
-
.stat {
|
|
161
|
-
display: flex;
|
|
162
|
-
justify-content: space-between;
|
|
163
|
-
margin-bottom: 8px;
|
|
164
|
-
}
|
|
165
|
-
.label {
|
|
166
|
-
font-weight: 500;
|
|
167
|
-
color: #374151;
|
|
168
|
-
}
|
|
169
|
-
.value {
|
|
170
|
-
font-weight: 600;
|
|
171
|
-
}
|
|
172
|
-
`}</style>
|
|
173
|
-
</div>
|
|
174
|
-
);
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
// Composant pour une carte de shard
|
|
178
|
-
const ShardCard = ({ shard }: { shard: ShardInfo }) => {
|
|
179
|
-
const statusColor =
|
|
180
|
-
shard.status === 'active' ? '#22c55e' :
|
|
181
|
-
shard.status === 'draining' ? '#f59e0b' :
|
|
182
|
-
shard.status === 'maintenance' ? '#3b82f6' : '#6b7280';
|
|
183
|
-
|
|
184
|
-
const utilizationPercentage = Math.round((shard.connections / shard.capacity) * 100) || 0;
|
|
185
|
-
|
|
186
|
-
// Formatage de l'ID du shard pour l'affichage (prendre les derniers caractères ou tout l'ID)
|
|
187
|
-
const displayId = shard.id.includes('-') ? shard.id.split('-').pop() : shard.id;
|
|
188
|
-
|
|
189
|
-
return (
|
|
190
|
-
<div className="shard-card">
|
|
191
|
-
<div className="shard-header">
|
|
192
|
-
<h4 className="shard-title">Shard: {displayId}</h4>
|
|
193
|
-
<div className="status-badge" style={{ backgroundColor: statusColor }}>
|
|
194
|
-
{shard.status || 'unknown'}
|
|
195
|
-
</div>
|
|
196
|
-
</div>
|
|
197
|
-
|
|
198
|
-
<div className="shard-url">{shard.url}</div>
|
|
199
|
-
|
|
200
|
-
<div className="shard-stats">
|
|
201
|
-
<div className="stat">
|
|
202
|
-
<span className="label">Connexions:</span>
|
|
203
|
-
<span className="value">{shard.connections} / {shard.capacity}</span>
|
|
204
|
-
</div>
|
|
205
|
-
<div className="stat">
|
|
206
|
-
<span className="label">Utilisation:</span>
|
|
207
|
-
</div>
|
|
208
|
-
<ProgressBar percentage={utilizationPercentage} />
|
|
209
|
-
</div>
|
|
210
|
-
|
|
211
|
-
{/* @ts-ignore */}
|
|
212
|
-
<style jsx>{`
|
|
213
|
-
.shard-card {
|
|
214
|
-
background: #f9fafb;
|
|
215
|
-
border-radius: 6px;
|
|
216
|
-
padding: 12px;
|
|
217
|
-
margin-bottom: 12px;
|
|
218
|
-
border: 1px solid #e5e7eb;
|
|
219
|
-
}
|
|
220
|
-
.shard-header {
|
|
221
|
-
display: flex;
|
|
222
|
-
justify-content: space-between;
|
|
223
|
-
align-items: center;
|
|
224
|
-
margin-bottom: 8px;
|
|
225
|
-
}
|
|
226
|
-
.shard-title {
|
|
227
|
-
margin: 0;
|
|
228
|
-
font-size: 1rem;
|
|
229
|
-
font-weight: 600;
|
|
230
|
-
}
|
|
231
|
-
.status-badge {
|
|
232
|
-
padding: 4px 8px;
|
|
233
|
-
border-radius: 9999px;
|
|
234
|
-
font-size: 0.75rem;
|
|
235
|
-
color: white;
|
|
236
|
-
text-transform: capitalize;
|
|
237
|
-
}
|
|
238
|
-
.shard-url {
|
|
239
|
-
font-family: monospace;
|
|
240
|
-
font-size: 0.8rem;
|
|
241
|
-
color: #6b7280;
|
|
242
|
-
margin-bottom: 8px;
|
|
243
|
-
word-break: break-all;
|
|
244
|
-
}
|
|
245
|
-
.shard-stats {
|
|
246
|
-
margin-top: 8px;
|
|
247
|
-
}
|
|
248
|
-
.stat {
|
|
249
|
-
display: flex;
|
|
250
|
-
justify-content: space-between;
|
|
251
|
-
margin-bottom: 6px;
|
|
252
|
-
font-size: 0.9rem;
|
|
253
|
-
}
|
|
254
|
-
.label {
|
|
255
|
-
font-weight: 500;
|
|
256
|
-
color: #374151;
|
|
257
|
-
}
|
|
258
|
-
.value {
|
|
259
|
-
font-weight: 600;
|
|
260
|
-
}
|
|
261
|
-
`}</style>
|
|
262
|
-
</div>
|
|
263
|
-
);
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
// Formulaire pour créer une salle
|
|
267
|
-
const CreateRoomForm = ({ worldId, onRoomCreated }: { worldId: string; onRoomCreated: () => void }) => {
|
|
268
|
-
const [name, setName] = useState('');
|
|
269
|
-
const [balancingStrategy, setBalancingStrategy] = useState<'round-robin' | 'least-connections' | 'random'>('round-robin');
|
|
270
|
-
const [isPublic, setIsPublic] = useState(true);
|
|
271
|
-
const [maxPlayers, setMaxPlayers] = useState(100);
|
|
272
|
-
const [minShards, setMinShards] = useState(1);
|
|
273
|
-
const [maxShards, setMaxShards] = useState(10);
|
|
274
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
275
|
-
const [error, setError] = useState('');
|
|
276
|
-
|
|
277
|
-
const handleSubmit = async (e: React.FormEvent) => {
|
|
278
|
-
e.preventDefault();
|
|
279
|
-
setIsLoading(true);
|
|
280
|
-
setError('');
|
|
281
|
-
|
|
282
|
-
try {
|
|
283
|
-
// Utiliser une requête HTTP
|
|
284
|
-
const response = await fetch(`/parties/world/world-default/register-room`, {
|
|
285
|
-
method: 'POST',
|
|
286
|
-
headers: {
|
|
287
|
-
'Content-Type': 'application/json',
|
|
288
|
-
},
|
|
289
|
-
body: JSON.stringify({
|
|
290
|
-
roomId: name,
|
|
291
|
-
config: {
|
|
292
|
-
name,
|
|
293
|
-
balancingStrategy,
|
|
294
|
-
public: isPublic,
|
|
295
|
-
maxPlayersPerShard: maxPlayers,
|
|
296
|
-
minShards,
|
|
297
|
-
maxShards
|
|
298
|
-
}
|
|
299
|
-
})
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
if (!response.ok) {
|
|
303
|
-
const errorData = await response.json();
|
|
304
|
-
throw new Error(errorData.error || "Échec de la création de la salle");
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
onRoomCreated();
|
|
308
|
-
|
|
309
|
-
// Reset form
|
|
310
|
-
setName('');
|
|
311
|
-
setBalancingStrategy('round-robin');
|
|
312
|
-
setIsPublic(true);
|
|
313
|
-
setMaxPlayers(100);
|
|
314
|
-
setMinShards(1);
|
|
315
|
-
setMaxShards(10);
|
|
316
|
-
} catch (err: any) {
|
|
317
|
-
setError(err.message);
|
|
318
|
-
} finally {
|
|
319
|
-
setIsLoading(false);
|
|
320
|
-
}
|
|
321
|
-
};
|
|
322
|
-
|
|
323
|
-
return (
|
|
324
|
-
<div className="create-room-form">
|
|
325
|
-
<h3>Créer une nouvelle salle</h3>
|
|
326
|
-
|
|
327
|
-
{error && <div className="error-message">{error}</div>}
|
|
328
|
-
|
|
329
|
-
<form onSubmit={handleSubmit}>
|
|
330
|
-
<div className="form-group">
|
|
331
|
-
<label htmlFor="name">Nom de la salle</label>
|
|
332
|
-
<input
|
|
333
|
-
type="text"
|
|
334
|
-
id="name"
|
|
335
|
-
value={name}
|
|
336
|
-
onChange={(e) => setName(e.target.value)}
|
|
337
|
-
required
|
|
338
|
-
/>
|
|
339
|
-
</div>
|
|
340
|
-
|
|
341
|
-
<div className="form-group">
|
|
342
|
-
<label htmlFor="balancingStrategy">Stratégie d'équilibrage</label>
|
|
343
|
-
<select
|
|
344
|
-
id="balancingStrategy"
|
|
345
|
-
value={balancingStrategy}
|
|
346
|
-
onChange={(e) => setBalancingStrategy(e.target.value as any)}
|
|
347
|
-
required
|
|
348
|
-
>
|
|
349
|
-
<option value="round-robin">Round Robin</option>
|
|
350
|
-
<option value="least-connections">Moins de connexions</option>
|
|
351
|
-
<option value="random">Aléatoire</option>
|
|
352
|
-
</select>
|
|
353
|
-
</div>
|
|
354
|
-
|
|
355
|
-
<div className="form-group">
|
|
356
|
-
<label htmlFor="isPublic">Visibilité</label>
|
|
357
|
-
<div className="toggle-container">
|
|
358
|
-
<input
|
|
359
|
-
type="checkbox"
|
|
360
|
-
id="isPublic"
|
|
361
|
-
checked={isPublic}
|
|
362
|
-
onChange={(e) => setIsPublic(e.target.checked)}
|
|
363
|
-
/>
|
|
364
|
-
<label htmlFor="isPublic" className="toggle-label">
|
|
365
|
-
{isPublic ? 'Publique' : 'Privée'}
|
|
366
|
-
</label>
|
|
367
|
-
</div>
|
|
368
|
-
</div>
|
|
369
|
-
|
|
370
|
-
<div className="form-group">
|
|
371
|
-
<label htmlFor="maxPlayers">Joueurs max par shard</label>
|
|
372
|
-
<input
|
|
373
|
-
type="number"
|
|
374
|
-
id="maxPlayers"
|
|
375
|
-
value={maxPlayers}
|
|
376
|
-
onChange={(e) => setMaxPlayers(parseInt(e.target.value))}
|
|
377
|
-
min="1"
|
|
378
|
-
required
|
|
379
|
-
/>
|
|
380
|
-
</div>
|
|
381
|
-
|
|
382
|
-
<div className="form-group">
|
|
383
|
-
<label htmlFor="minShards">Nombre min. de shards</label>
|
|
384
|
-
<input
|
|
385
|
-
type="number"
|
|
386
|
-
id="minShards"
|
|
387
|
-
value={minShards}
|
|
388
|
-
onChange={(e) => setMinShards(parseInt(e.target.value))}
|
|
389
|
-
min="1"
|
|
390
|
-
required
|
|
391
|
-
/>
|
|
392
|
-
</div>
|
|
393
|
-
|
|
394
|
-
<div className="form-group">
|
|
395
|
-
<label htmlFor="maxShards">Nombre max. de shards</label>
|
|
396
|
-
<input
|
|
397
|
-
type="number"
|
|
398
|
-
id="maxShards"
|
|
399
|
-
value={maxShards}
|
|
400
|
-
onChange={(e) => setMaxShards(parseInt(e.target.value))}
|
|
401
|
-
min={minShards}
|
|
402
|
-
required
|
|
403
|
-
/>
|
|
404
|
-
</div>
|
|
405
|
-
|
|
406
|
-
<button type="submit" className="submit-button" disabled={isLoading}>
|
|
407
|
-
{isLoading ? 'Création...' : 'Créer la salle'}
|
|
408
|
-
</button>
|
|
409
|
-
</form>
|
|
410
|
-
|
|
411
|
-
{/* @ts-ignore */}
|
|
412
|
-
<style jsx>{`
|
|
413
|
-
.create-room-form {
|
|
414
|
-
background: white;
|
|
415
|
-
border-radius: 8px;
|
|
416
|
-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
417
|
-
padding: 16px;
|
|
418
|
-
margin-bottom: 24px;
|
|
419
|
-
}
|
|
420
|
-
.error-message {
|
|
421
|
-
background-color: #fee2e2;
|
|
422
|
-
border: 1px solid #ef4444;
|
|
423
|
-
color: #b91c1c;
|
|
424
|
-
padding: 8px 12px;
|
|
425
|
-
border-radius: 4px;
|
|
426
|
-
margin-bottom: 16px;
|
|
427
|
-
}
|
|
428
|
-
.form-group {
|
|
429
|
-
margin-bottom: 16px;
|
|
430
|
-
}
|
|
431
|
-
label {
|
|
432
|
-
display: block;
|
|
433
|
-
margin-bottom: 6px;
|
|
434
|
-
font-weight: 500;
|
|
435
|
-
color: #374151;
|
|
436
|
-
}
|
|
437
|
-
input[type="text"],
|
|
438
|
-
input[type="number"],
|
|
439
|
-
select {
|
|
440
|
-
width: 100%;
|
|
441
|
-
padding: 8px 12px;
|
|
442
|
-
border: 1px solid #d1d5db;
|
|
443
|
-
border-radius: 4px;
|
|
444
|
-
font-size: 16px;
|
|
445
|
-
}
|
|
446
|
-
input[type="checkbox"] {
|
|
447
|
-
margin-right: 8px;
|
|
448
|
-
}
|
|
449
|
-
.toggle-container {
|
|
450
|
-
display: flex;
|
|
451
|
-
align-items: center;
|
|
452
|
-
}
|
|
453
|
-
.toggle-label {
|
|
454
|
-
margin-bottom: 0;
|
|
455
|
-
}
|
|
456
|
-
.submit-button {
|
|
457
|
-
background-color: #2563eb;
|
|
458
|
-
color: white;
|
|
459
|
-
border: none;
|
|
460
|
-
border-radius: 4px;
|
|
461
|
-
padding: 8px 16px;
|
|
462
|
-
font-size: 16px;
|
|
463
|
-
cursor: pointer;
|
|
464
|
-
transition: background-color 0.2s;
|
|
465
|
-
}
|
|
466
|
-
.submit-button:hover {
|
|
467
|
-
background-color: #1d4ed8;
|
|
468
|
-
}
|
|
469
|
-
.submit-button:disabled {
|
|
470
|
-
background-color: #93c5fd;
|
|
471
|
-
cursor: not-allowed;
|
|
472
|
-
}
|
|
473
|
-
`}</style>
|
|
474
|
-
</div>
|
|
475
|
-
);
|
|
476
|
-
};
|
|
477
|
-
|
|
478
|
-
// Formulaire pour le scaling d'une salle
|
|
479
|
-
const ScaleRoomForm = ({ worldId, room, onScaled }: { worldId: string; room: RoomInfo; onScaled: () => void }) => {
|
|
480
|
-
const [targetShardCount, setTargetShardCount] = useState(room.shards.length);
|
|
481
|
-
const [urlTemplate, setUrlTemplate] = useState('wss://shard-{shardId}.example.com');
|
|
482
|
-
const [maxConnections, setMaxConnections] = useState(room.config.maxPlayersPerShard);
|
|
483
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
484
|
-
const [error, setError] = useState('');
|
|
485
|
-
|
|
486
|
-
// Met à jour les valeurs par défaut quand la salle change
|
|
487
|
-
useEffect(() => {
|
|
488
|
-
setTargetShardCount(room.shards.length);
|
|
489
|
-
setMaxConnections(room.config.maxPlayersPerShard);
|
|
490
|
-
// Essaie de déduire un modèle d'URL à partir des shards existants
|
|
491
|
-
if (room.shards.length > 0) {
|
|
492
|
-
const sampleUrl = room.shards[0].url;
|
|
493
|
-
setUrlTemplate(sampleUrl.replace(room.shards[0].id, '{shardId}'));
|
|
494
|
-
}
|
|
495
|
-
}, [room]);
|
|
496
|
-
|
|
497
|
-
const handleSubmit = async (e: React.FormEvent) => {
|
|
498
|
-
e.preventDefault();
|
|
499
|
-
setIsLoading(true);
|
|
500
|
-
setError('');
|
|
501
|
-
|
|
502
|
-
try {
|
|
503
|
-
// Utiliser une requête HTTP
|
|
504
|
-
const response = await fetch(`/api/world/scale-room`, {
|
|
505
|
-
method: 'POST',
|
|
506
|
-
headers: {
|
|
507
|
-
'Content-Type': 'application/json',
|
|
508
|
-
},
|
|
509
|
-
body: JSON.stringify({
|
|
510
|
-
roomId: room.roomId,
|
|
511
|
-
targetShardCount,
|
|
512
|
-
shardTemplate: {
|
|
513
|
-
urlTemplate,
|
|
514
|
-
maxConnections
|
|
515
|
-
}
|
|
516
|
-
})
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
if (!response.ok) {
|
|
520
|
-
const errorData = await response.json();
|
|
521
|
-
throw new Error(errorData.error || "Échec de la mise à jour des shards");
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
onScaled();
|
|
525
|
-
} catch (err: any) {
|
|
526
|
-
setError(err.message);
|
|
527
|
-
} finally {
|
|
528
|
-
setIsLoading(false);
|
|
529
|
-
}
|
|
530
|
-
};
|
|
531
|
-
|
|
532
|
-
return (
|
|
533
|
-
<div className="scale-room-form">
|
|
534
|
-
<h3>Ajuster le nombre de shards</h3>
|
|
535
|
-
|
|
536
|
-
{error && <div className="error-message">{error}</div>}
|
|
537
|
-
|
|
538
|
-
<form onSubmit={handleSubmit}>
|
|
539
|
-
<div className="form-group">
|
|
540
|
-
<label htmlFor="targetShardCount">Nombre cible de shards</label>
|
|
541
|
-
<input
|
|
542
|
-
type="number"
|
|
543
|
-
id="targetShardCount"
|
|
544
|
-
value={targetShardCount}
|
|
545
|
-
onChange={(e) => setTargetShardCount(parseInt(e.target.value))}
|
|
546
|
-
min={room.config.minShards}
|
|
547
|
-
max={room.config.maxShards || 100}
|
|
548
|
-
required
|
|
549
|
-
/>
|
|
550
|
-
<div className="constraints">
|
|
551
|
-
Min: {room.config.minShards}, Max: {room.config.maxShards || '∞'}
|
|
552
|
-
</div>
|
|
553
|
-
</div>
|
|
554
|
-
|
|
555
|
-
<div className="form-group">
|
|
556
|
-
<label htmlFor="urlTemplate">Modèle d'URL pour nouveaux shards</label>
|
|
557
|
-
<input
|
|
558
|
-
type="text"
|
|
559
|
-
id="urlTemplate"
|
|
560
|
-
value={urlTemplate}
|
|
561
|
-
onChange={(e) => setUrlTemplate(e.target.value)}
|
|
562
|
-
required
|
|
563
|
-
/>
|
|
564
|
-
<div className="hint">Utilisez {'{shardId}'} comme placeholder</div>
|
|
565
|
-
</div>
|
|
566
|
-
|
|
567
|
-
<div className="form-group">
|
|
568
|
-
<label htmlFor="maxConnections">Connexions max par shard</label>
|
|
569
|
-
<input
|
|
570
|
-
type="number"
|
|
571
|
-
id="maxConnections"
|
|
572
|
-
value={maxConnections}
|
|
573
|
-
onChange={(e) => setMaxConnections(parseInt(e.target.value))}
|
|
574
|
-
min="1"
|
|
575
|
-
required
|
|
576
|
-
/>
|
|
577
|
-
</div>
|
|
578
|
-
|
|
579
|
-
<button type="submit" className="submit-button" disabled={isLoading}>
|
|
580
|
-
{isLoading ? 'Mise à jour...' : 'Mettre à jour les shards'}
|
|
581
|
-
</button>
|
|
582
|
-
</form>
|
|
583
|
-
|
|
584
|
-
{/* @ts-ignore */}
|
|
585
|
-
<style jsx>{`
|
|
586
|
-
.scale-room-form {
|
|
587
|
-
background: white;
|
|
588
|
-
border-radius: 8px;
|
|
589
|
-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
590
|
-
padding: 16px;
|
|
591
|
-
margin-bottom: 24px;
|
|
592
|
-
}
|
|
593
|
-
.error-message {
|
|
594
|
-
background-color: #fee2e2;
|
|
595
|
-
border: 1px solid #ef4444;
|
|
596
|
-
color: #b91c1c;
|
|
597
|
-
padding: 8px 12px;
|
|
598
|
-
border-radius: 4px;
|
|
599
|
-
margin-bottom: 16px;
|
|
600
|
-
}
|
|
601
|
-
.form-group {
|
|
602
|
-
margin-bottom: 16px;
|
|
603
|
-
}
|
|
604
|
-
label {
|
|
605
|
-
display: block;
|
|
606
|
-
margin-bottom: 6px;
|
|
607
|
-
font-weight: 500;
|
|
608
|
-
color: #374151;
|
|
609
|
-
}
|
|
610
|
-
input[type="text"],
|
|
611
|
-
input[type="number"] {
|
|
612
|
-
width: 100%;
|
|
613
|
-
padding: 8px 12px;
|
|
614
|
-
border: 1px solid #d1d5db;
|
|
615
|
-
border-radius: 4px;
|
|
616
|
-
font-size: 16px;
|
|
617
|
-
}
|
|
618
|
-
.constraints, .hint {
|
|
619
|
-
margin-top: 4px;
|
|
620
|
-
font-size: 0.8rem;
|
|
621
|
-
color: #6b7280;
|
|
622
|
-
}
|
|
623
|
-
.submit-button {
|
|
624
|
-
background-color: #2563eb;
|
|
625
|
-
color: white;
|
|
626
|
-
border: none;
|
|
627
|
-
border-radius: 4px;
|
|
628
|
-
padding: 8px 16px;
|
|
629
|
-
font-size: 16px;
|
|
630
|
-
cursor: pointer;
|
|
631
|
-
transition: background-color 0.2s;
|
|
632
|
-
}
|
|
633
|
-
.submit-button:hover {
|
|
634
|
-
background-color: #1d4ed8;
|
|
635
|
-
}
|
|
636
|
-
.submit-button:disabled {
|
|
637
|
-
background-color: #93c5fd;
|
|
638
|
-
cursor: not-allowed;
|
|
639
|
-
}
|
|
640
|
-
`}</style>
|
|
641
|
-
</div>
|
|
642
|
-
);
|
|
643
|
-
};
|
|
644
|
-
|
|
645
|
-
// Fonction pour convertir les données du world en format RoomInfo
|
|
646
|
-
const parseWorldInfoFromClient = (worldClient: WorldClient): WorldInfo => {
|
|
647
|
-
// Récupérer les données du client
|
|
648
|
-
const roomsData = worldClient.rooms();
|
|
649
|
-
const shardsData = worldClient.shards();
|
|
650
|
-
const roomShardsData = worldClient.roomShards();
|
|
651
|
-
|
|
652
|
-
// Transformer les données
|
|
653
|
-
const roomsInfo = Object.keys(roomsData).map(roomId => {
|
|
654
|
-
const room = roomsData[roomId];
|
|
655
|
-
const roomShardIds = roomShardsData[roomId] || [];
|
|
656
|
-
const roomShards = roomShardIds
|
|
657
|
-
.map(id => shardsData[id])
|
|
658
|
-
.filter(Boolean);
|
|
659
|
-
|
|
660
|
-
// Calculer les métriques
|
|
661
|
-
const totalConnections = roomShards.reduce(
|
|
662
|
-
(sum, shard) => sum + (shard ? shard.currentConnections : 0),
|
|
663
|
-
0
|
|
664
|
-
);
|
|
665
|
-
|
|
666
|
-
const totalCapacity = roomShards.reduce(
|
|
667
|
-
(sum, shard) => sum + (shard ? shard.maxConnections : 0),
|
|
668
|
-
0
|
|
669
|
-
);
|
|
670
|
-
|
|
671
|
-
const utilizationPercentage = totalCapacity > 0
|
|
672
|
-
? Math.round((totalConnections / totalCapacity) * 100)
|
|
673
|
-
: 0;
|
|
674
|
-
|
|
675
|
-
return {
|
|
676
|
-
roomId,
|
|
677
|
-
config: {
|
|
678
|
-
name: room ? room.name : '',
|
|
679
|
-
balancingStrategy: room ? room.balancingStrategy : 'round-robin',
|
|
680
|
-
public: room ? room.public : true,
|
|
681
|
-
maxPlayersPerShard: room ? room.maxPlayersPerShard : 100,
|
|
682
|
-
minShards: room ? room.minShards : 1,
|
|
683
|
-
maxShards: room ? room.maxShards : undefined
|
|
684
|
-
},
|
|
685
|
-
shards: roomShards.map(shard => ({
|
|
686
|
-
id: shard ? shard.id || '' : '',
|
|
687
|
-
url: shard ? shard.url : '',
|
|
688
|
-
connections: shard ? shard.currentConnections : 0,
|
|
689
|
-
capacity: shard ? shard.maxConnections : 0,
|
|
690
|
-
status: shard ? shard.status : 'unknown'
|
|
691
|
-
})),
|
|
692
|
-
metrics: {
|
|
693
|
-
totalConnections,
|
|
694
|
-
totalCapacity,
|
|
695
|
-
utilizationPercentage
|
|
696
|
-
}
|
|
697
|
-
};
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
return { rooms: roomsInfo };
|
|
701
|
-
};
|
|
702
|
-
|
|
703
|
-
// Composant principal de l'administration
|
|
704
|
-
const WorldAdmin = () => {
|
|
705
|
-
const [worldId, setWorldId] = useState('default');
|
|
706
|
-
const [worldInfo, setWorldInfo] = useState<WorldInfo | null>(null);
|
|
707
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
708
|
-
const [selectedRoomId, setSelectedRoomId] = useState<string | null>(null);
|
|
709
|
-
const [error, setError] = useState<string | null>(null);
|
|
710
|
-
const [refresh, setRefresh] = useState(0);
|
|
711
|
-
|
|
712
|
-
// Références pour le client world et la connexion
|
|
713
|
-
const worldClient = useRef<WorldClient>(new WorldClient());
|
|
714
|
-
const connection = useRef<any>(null);
|
|
715
|
-
|
|
716
|
-
// Se connecter au world via WebSocket
|
|
717
|
-
useEffect(() => {
|
|
718
|
-
const connectToWorld = async () => {
|
|
719
|
-
setIsLoading(true);
|
|
720
|
-
setError(null);
|
|
721
|
-
|
|
722
|
-
try {
|
|
723
|
-
// Réinitialiser le client world
|
|
724
|
-
worldClient.current = new WorldClient();
|
|
725
|
-
|
|
726
|
-
// Se connecter au world
|
|
727
|
-
const conn = await connectionRoom({
|
|
728
|
-
host: window.location.origin,
|
|
729
|
-
room: `world-default`,
|
|
730
|
-
party: 'world',
|
|
731
|
-
query: {
|
|
732
|
-
'world-auth-token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30'
|
|
733
|
-
}
|
|
734
|
-
}, worldClient.current);
|
|
735
|
-
|
|
736
|
-
connection.current = conn;
|
|
737
|
-
|
|
738
|
-
// Configurer les écouteurs d'événements
|
|
739
|
-
conn.on('sync', (data) => {
|
|
740
|
-
// Mettre à jour les signaux du client uniquement si les données correspondantes sont définies
|
|
741
|
-
if (data.rooms) {
|
|
742
|
-
const currentRooms = worldClient.current.rooms();
|
|
743
|
-
const updatedRooms = { ...currentRooms };
|
|
744
|
-
|
|
745
|
-
// Fusionner les nouvelles données avec les données existantes
|
|
746
|
-
Object.entries(data.rooms).forEach(([roomId, roomData]: [string, any]) => {
|
|
747
|
-
updatedRooms[roomId] = {
|
|
748
|
-
...(updatedRooms[roomId] || {}),
|
|
749
|
-
...roomData
|
|
750
|
-
};
|
|
751
|
-
});
|
|
752
|
-
|
|
753
|
-
worldClient.current.rooms.set(updatedRooms);
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
if (data.shards) {
|
|
757
|
-
const currentShards = worldClient.current.shards();
|
|
758
|
-
const updatedShards = { ...currentShards };
|
|
759
|
-
|
|
760
|
-
// Fusionner les nouvelles données avec les données existantes
|
|
761
|
-
Object.entries(data.shards).forEach(([shardId, shardData]: [string, any]) => {
|
|
762
|
-
updatedShards[shardId] = {
|
|
763
|
-
...(updatedShards[shardId] || {}),
|
|
764
|
-
...shardData,
|
|
765
|
-
// Toujours s'assurer que l'ID est défini
|
|
766
|
-
id: shardId
|
|
767
|
-
};
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
worldClient.current.shards.set(updatedShards);
|
|
771
|
-
|
|
772
|
-
// Mettre à jour roomShards si nécessaire
|
|
773
|
-
// Ne reconstruire roomShards que si nous avons reçu des nouveaux shards avec roomId
|
|
774
|
-
const shouldUpdateRoomShards = Object.values(data.shards).some((shard: any) => shard.roomId);
|
|
775
|
-
|
|
776
|
-
if (shouldUpdateRoomShards) {
|
|
777
|
-
const currentRoomShards = worldClient.current.roomShards();
|
|
778
|
-
const updatedRoomShards = { ...currentRoomShards };
|
|
779
|
-
|
|
780
|
-
// Parcourir tous les shards pour construire la relation room -> shards
|
|
781
|
-
Object.entries(updatedShards).forEach(([shardId, shardData]: [string, any]) => {
|
|
782
|
-
const roomId = shardData.roomId;
|
|
783
|
-
if (roomId) {
|
|
784
|
-
if (!updatedRoomShards[roomId]) {
|
|
785
|
-
updatedRoomShards[roomId] = [];
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
// Ajouter le shardId s'il n'est pas déjà présent
|
|
789
|
-
if (!updatedRoomShards[roomId].includes(shardId)) {
|
|
790
|
-
updatedRoomShards[roomId].push(shardId);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
});
|
|
794
|
-
|
|
795
|
-
// Mettre à jour le signal roomShards avec les nouvelles données
|
|
796
|
-
worldClient.current.roomShards.set(updatedRoomShards);
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// Déclencher une mise à jour du composant
|
|
801
|
-
setRefresh(prev => prev + 1);
|
|
802
|
-
});
|
|
803
|
-
|
|
804
|
-
conn.on('error', (err: any) => {
|
|
805
|
-
setError(`Erreur de connexion: ${err.message}`);
|
|
806
|
-
});
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
setIsLoading(false);
|
|
810
|
-
} catch (err: any) {
|
|
811
|
-
setError(`Erreur de connexion: ${err.message}`);
|
|
812
|
-
setIsLoading(false);
|
|
813
|
-
}
|
|
814
|
-
};
|
|
815
|
-
|
|
816
|
-
connectToWorld();
|
|
817
|
-
|
|
818
|
-
return () => {
|
|
819
|
-
// Nettoyer la connexion lors du démontage
|
|
820
|
-
if (connection.current) {
|
|
821
|
-
connection.current.close();
|
|
822
|
-
}
|
|
823
|
-
};
|
|
824
|
-
}, [worldId]);
|
|
825
|
-
|
|
826
|
-
// Mettre à jour worldInfo lorsque les données du client changent
|
|
827
|
-
useEffect(() => {
|
|
828
|
-
// Utiliser les données du client pour générer worldInfo
|
|
829
|
-
const info = parseWorldInfoFromClient(worldClient.current);
|
|
830
|
-
setWorldInfo(info);
|
|
831
|
-
|
|
832
|
-
// Sélectionner la première salle par défaut si aucune n'est sélectionnée
|
|
833
|
-
if (!selectedRoomId && info.rooms && info.rooms.length > 0) {
|
|
834
|
-
setSelectedRoomId(info.rooms[0].roomId);
|
|
835
|
-
} else if (selectedRoomId && !info.rooms.some(room => room.roomId === selectedRoomId)) {
|
|
836
|
-
// Si la salle sélectionnée n'existe plus, sélectionner la première salle ou null
|
|
837
|
-
setSelectedRoomId(info.rooms.length > 0 ? info.rooms[0].roomId : null);
|
|
838
|
-
}
|
|
839
|
-
}, [refresh, selectedRoomId]);
|
|
840
|
-
|
|
841
|
-
// Salle sélectionnée
|
|
842
|
-
const selectedRoom = worldInfo?.rooms.find(room => room.roomId === selectedRoomId);
|
|
843
|
-
|
|
844
|
-
// Gestion du rafraîchissement manuel
|
|
845
|
-
const handleRefresh = () => {
|
|
846
|
-
|
|
847
|
-
};
|
|
848
|
-
|
|
849
|
-
// Handler pour la création d'une salle
|
|
850
|
-
const handleRoomCreated = () => {
|
|
851
|
-
handleRefresh();
|
|
852
|
-
};
|
|
853
|
-
|
|
854
|
-
// Handler pour le scaling d'une salle
|
|
855
|
-
const handleRoomScaled = () => {
|
|
856
|
-
handleRefresh();
|
|
857
|
-
};
|
|
858
|
-
|
|
859
|
-
return (
|
|
860
|
-
<div className="world-admin">
|
|
861
|
-
<div className="admin-header">
|
|
862
|
-
<h1>Administration World</h1>
|
|
863
|
-
<div className="world-selector">
|
|
864
|
-
<label htmlFor="worldId">ID du World:</label>
|
|
865
|
-
<input
|
|
866
|
-
type="text"
|
|
867
|
-
id="worldId"
|
|
868
|
-
value={worldId}
|
|
869
|
-
onChange={(e) => setWorldId(e.target.value)}
|
|
870
|
-
/>
|
|
871
|
-
</div>
|
|
872
|
-
<button className="refresh-button" onClick={handleRefresh}>
|
|
873
|
-
Rafraîchir
|
|
874
|
-
</button>
|
|
875
|
-
</div>
|
|
876
|
-
|
|
877
|
-
{error && (
|
|
878
|
-
<div className="error-banner">
|
|
879
|
-
{error}
|
|
880
|
-
</div>
|
|
881
|
-
)}
|
|
882
|
-
|
|
883
|
-
{isLoading ? (
|
|
884
|
-
<LoadingSpinner />
|
|
885
|
-
) : (
|
|
886
|
-
<div className="admin-content">
|
|
887
|
-
<div className="rooms-list">
|
|
888
|
-
<h2>Salles ({worldInfo?.rooms.length || 0})</h2>
|
|
889
|
-
|
|
890
|
-
<CreateRoomForm
|
|
891
|
-
worldId={worldId}
|
|
892
|
-
onRoomCreated={handleRoomCreated}
|
|
893
|
-
/>
|
|
894
|
-
|
|
895
|
-
{worldInfo?.rooms.map(room => (
|
|
896
|
-
<RoomCard
|
|
897
|
-
key={room.roomId}
|
|
898
|
-
room={room}
|
|
899
|
-
onSelect={() => setSelectedRoomId(room.roomId)}
|
|
900
|
-
/>
|
|
901
|
-
))}
|
|
902
|
-
|
|
903
|
-
{worldInfo?.rooms.length === 0 && (
|
|
904
|
-
<div className="no-rooms">
|
|
905
|
-
Aucune salle enregistrée. Créez-en une avec le formulaire ci-dessus.
|
|
906
|
-
</div>
|
|
907
|
-
)}
|
|
908
|
-
</div>
|
|
909
|
-
|
|
910
|
-
<div className="room-details">
|
|
911
|
-
{selectedRoom ? (
|
|
912
|
-
<>
|
|
913
|
-
<div className="room-header">
|
|
914
|
-
<h2>{selectedRoom.config.name}</h2>
|
|
915
|
-
<div className="room-id-badge">{selectedRoom.roomId}</div>
|
|
916
|
-
</div>
|
|
917
|
-
|
|
918
|
-
<div className="room-metrics">
|
|
919
|
-
<div className="metric-card">
|
|
920
|
-
<div className="metric-title">Connexions totales</div>
|
|
921
|
-
<div className="metric-value">{selectedRoom.metrics.totalConnections} / {selectedRoom.metrics.totalCapacity}</div>
|
|
922
|
-
<ProgressBar percentage={selectedRoom.metrics.utilizationPercentage} />
|
|
923
|
-
</div>
|
|
924
|
-
|
|
925
|
-
<div className="metric-card">
|
|
926
|
-
<div className="metric-title">Nombre de shards</div>
|
|
927
|
-
<div className="metric-value">{selectedRoom.shards.length}</div>
|
|
928
|
-
<div className="metric-subtitle">
|
|
929
|
-
Min: {selectedRoom.config.minShards},
|
|
930
|
-
Max: {selectedRoom.config.maxShards || '∞'}
|
|
931
|
-
</div>
|
|
932
|
-
</div>
|
|
933
|
-
|
|
934
|
-
<div className="metric-card">
|
|
935
|
-
<div className="metric-title">Stratégie</div>
|
|
936
|
-
<div className="metric-value">{selectedRoom.config.balancingStrategy}</div>
|
|
937
|
-
<div className="metric-subtitle">
|
|
938
|
-
{selectedRoom.config.public ? 'Publique' : 'Privée'}
|
|
939
|
-
</div>
|
|
940
|
-
</div>
|
|
941
|
-
</div>
|
|
942
|
-
|
|
943
|
-
<ScaleRoomForm
|
|
944
|
-
worldId={worldId}
|
|
945
|
-
room={selectedRoom}
|
|
946
|
-
onScaled={handleRoomScaled}
|
|
947
|
-
/>
|
|
948
|
-
|
|
949
|
-
<h3>Shards ({selectedRoom.shards.length})</h3>
|
|
950
|
-
|
|
951
|
-
<div className="shards-grid">
|
|
952
|
-
{selectedRoom.shards.map(shard => (
|
|
953
|
-
<ShardCard key={shard.id} shard={shard} />
|
|
954
|
-
))}
|
|
955
|
-
|
|
956
|
-
{selectedRoom.shards.length === 0 && (
|
|
957
|
-
<div className="no-shards">
|
|
958
|
-
Aucun shard pour cette salle. Utilisez le formulaire ci-dessus pour en ajouter.
|
|
959
|
-
</div>
|
|
960
|
-
)}
|
|
961
|
-
</div>
|
|
962
|
-
</>
|
|
963
|
-
) : (
|
|
964
|
-
<div className="no-room-selected">
|
|
965
|
-
Sélectionnez une salle dans la liste pour voir ses détails.
|
|
966
|
-
</div>
|
|
967
|
-
)}
|
|
968
|
-
</div>
|
|
969
|
-
</div>
|
|
970
|
-
)}
|
|
971
|
-
|
|
972
|
-
<Room />
|
|
973
|
-
|
|
974
|
-
{/* @ts-ignore */}
|
|
975
|
-
<style jsx>{`
|
|
976
|
-
.world-admin {
|
|
977
|
-
padding: 20px;
|
|
978
|
-
max-width: 1200px;
|
|
979
|
-
margin: 0 auto;
|
|
980
|
-
}
|
|
981
|
-
.admin-header {
|
|
982
|
-
display: flex;
|
|
983
|
-
justify-content: space-between;
|
|
984
|
-
align-items: center;
|
|
985
|
-
margin-bottom: 24px;
|
|
986
|
-
}
|
|
987
|
-
.world-selector {
|
|
988
|
-
display: flex;
|
|
989
|
-
align-items: center;
|
|
990
|
-
}
|
|
991
|
-
.world-selector label {
|
|
992
|
-
margin-right: 8px;
|
|
993
|
-
}
|
|
994
|
-
.world-selector input {
|
|
995
|
-
padding: 6px 12px;
|
|
996
|
-
border: 1px solid #d1d5db;
|
|
997
|
-
border-radius: 4px;
|
|
998
|
-
}
|
|
999
|
-
.refresh-button {
|
|
1000
|
-
background-color: #f3f4f6;
|
|
1001
|
-
border: 1px solid #d1d5db;
|
|
1002
|
-
border-radius: 4px;
|
|
1003
|
-
padding: 6px 12px;
|
|
1004
|
-
cursor: pointer;
|
|
1005
|
-
transition: background-color 0.2s;
|
|
1006
|
-
}
|
|
1007
|
-
.refresh-button:hover {
|
|
1008
|
-
background-color: #e5e7eb;
|
|
1009
|
-
}
|
|
1010
|
-
.error-banner {
|
|
1011
|
-
background-color: #fee2e2;
|
|
1012
|
-
border: 1px solid #ef4444;
|
|
1013
|
-
color: #b91c1c;
|
|
1014
|
-
padding: 12px 16px;
|
|
1015
|
-
border-radius: 4px;
|
|
1016
|
-
margin-bottom: 24px;
|
|
1017
|
-
}
|
|
1018
|
-
.admin-content {
|
|
1019
|
-
display: grid;
|
|
1020
|
-
grid-template-columns: 1fr 2fr;
|
|
1021
|
-
gap: 24px;
|
|
1022
|
-
}
|
|
1023
|
-
.rooms-list {
|
|
1024
|
-
background: #f9fafb;
|
|
1025
|
-
border-radius: 8px;
|
|
1026
|
-
padding: 16px;
|
|
1027
|
-
}
|
|
1028
|
-
.room-details {
|
|
1029
|
-
background: #f9fafb;
|
|
1030
|
-
border-radius: 8px;
|
|
1031
|
-
padding: 16px;
|
|
1032
|
-
}
|
|
1033
|
-
.room-header {
|
|
1034
|
-
display: flex;
|
|
1035
|
-
align-items: center;
|
|
1036
|
-
margin-bottom: 16px;
|
|
1037
|
-
}
|
|
1038
|
-
.room-id-badge {
|
|
1039
|
-
background: #e5e7eb;
|
|
1040
|
-
border-radius: 9999px;
|
|
1041
|
-
padding: 4px 12px;
|
|
1042
|
-
margin-left: 12px;
|
|
1043
|
-
font-size: 0.9rem;
|
|
1044
|
-
color: #374151;
|
|
1045
|
-
}
|
|
1046
|
-
.room-metrics {
|
|
1047
|
-
display: grid;
|
|
1048
|
-
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
1049
|
-
gap: 16px;
|
|
1050
|
-
margin-bottom: 24px;
|
|
1051
|
-
}
|
|
1052
|
-
.metric-card {
|
|
1053
|
-
background: white;
|
|
1054
|
-
border-radius: 8px;
|
|
1055
|
-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
1056
|
-
padding: 16px;
|
|
1057
|
-
}
|
|
1058
|
-
.metric-title {
|
|
1059
|
-
font-size: 0.9rem;
|
|
1060
|
-
color: #6b7280;
|
|
1061
|
-
margin-bottom: 8px;
|
|
1062
|
-
}
|
|
1063
|
-
.metric-value {
|
|
1064
|
-
font-size: 1.5rem;
|
|
1065
|
-
font-weight: 600;
|
|
1066
|
-
margin-bottom: 8px;
|
|
1067
|
-
}
|
|
1068
|
-
.metric-subtitle {
|
|
1069
|
-
font-size: 0.8rem;
|
|
1070
|
-
color: #6b7280;
|
|
1071
|
-
}
|
|
1072
|
-
.shards-grid {
|
|
1073
|
-
display: grid;
|
|
1074
|
-
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
1075
|
-
gap: 16px;
|
|
1076
|
-
}
|
|
1077
|
-
.no-rooms, .no-shards, .no-room-selected {
|
|
1078
|
-
background: white;
|
|
1079
|
-
border-radius: 8px;
|
|
1080
|
-
padding: 32px;
|
|
1081
|
-
text-align: center;
|
|
1082
|
-
color: #6b7280;
|
|
1083
|
-
}
|
|
1084
|
-
`}</style>
|
|
1085
|
-
</div>
|
|
1086
|
-
);
|
|
1087
|
-
};
|
|
1088
|
-
|
|
1089
|
-
export default WorldAdmin;
|