|
5 | 5 | import type { Server } from '$lib/proto/discopanel/v1/common_pb'; |
6 | 6 | import { ServerStatus } from '$lib/proto/discopanel/v1/common_pb'; |
7 | 7 | import type { LogEntry } from '$lib/proto/discopanel/v1/server_pb'; |
8 | | - import { GetServerLogsRequestSchema, ClearServerLogsRequestSchema, SendCommandRequestSchema } from '$lib/proto/discopanel/v1/server_pb'; |
| 8 | + import { GetServerLogsRequestSchema, ClearServerLogsRequestSchema, SendCommandRequestSchema, UploadToMCLogsRequestSchema } from '$lib/proto/discopanel/v1/server_pb'; |
9 | 9 | import { ResizablePaneGroup, ResizablePane, ResizableHandle } from '$lib/components/ui/resizable'; |
10 | 10 | import { Button } from '$lib/components/ui/button'; |
11 | 11 | import { Badge } from '$lib/components/ui/badge'; |
12 | 12 | import { toast } from 'svelte-sonner'; |
13 | | - import { Terminal, Send, Loader2, Download, Trash2, RefreshCw, Wifi, WifiOff } from '@lucide/svelte'; |
| 13 | + import { Terminal, Send, Loader2, Download, Upload, Trash2, RefreshCw, Wifi, WifiOff } from '@lucide/svelte'; |
| 14 | + import * as Tooltip from '$lib/components/ui/tooltip/index.js'; |
14 | 15 | import AnsiToHtml from 'ansi-to-html'; |
15 | 16 | import { getStringForEnum } from '$lib/utils'; |
16 | 17 | import { wsClient } from '$lib/stores/websocket.svelte'; |
|
216 | 217 | toast.success('Console cleared'); |
217 | 218 | } |
218 | 219 |
|
| 220 | + let uploading = $state(false); |
| 221 | +
|
| 222 | + async function uploadToMCLogs() { |
| 223 | + if (uploading) return; |
| 224 | + uploading = true; |
| 225 | + try { |
| 226 | + const request = create(UploadToMCLogsRequestSchema, { id: server.id }); |
| 227 | + const response = await rpcClient.server.uploadToMCLogs(request); |
| 228 | + await navigator.clipboard.writeText(response.url); |
| 229 | + toast.success('mclo.gs URL copied to clipboard'); |
| 230 | + } catch (error) { |
| 231 | + toast.error('Failed to upload to mclo.gs: ' + (error instanceof Error ? error.message : 'Unknown error')); |
| 232 | + } finally { |
| 233 | + uploading = false; |
| 234 | + } |
| 235 | + } |
| 236 | +
|
219 | 237 | function downloadLogs() { |
220 | 238 | const logText = logEntries.map(entry => entry.message).join('\n'); |
221 | 239 | const blob = new Blob([logText], { type: 'text/plain' }); |
|
270 | 288 | {/if} |
271 | 289 | </div> |
272 | 290 | <div class="flex items-center gap-1"> |
273 | | - <Button |
274 | | - size="sm" |
275 | | - variant="ghost" |
276 | | - onclick={fetchLogs} |
277 | | - disabled={loading} |
278 | | - class="h-7 w-7 p-0 text-zinc-400 hover:text-white" |
279 | | - > |
280 | | - {#if loading} |
281 | | - <Loader2 class="h-3 w-3 animate-spin" /> |
282 | | - {:else} |
283 | | - <RefreshCw class="h-3 w-3" /> |
284 | | - {/if} |
285 | | - </Button> |
286 | | - <Button |
287 | | - size="sm" |
288 | | - variant="ghost" |
289 | | - onclick={downloadLogs} |
290 | | - disabled={logEntries.length === 0} |
291 | | - class="h-7 w-7 p-0 text-zinc-400 hover:text-white" |
292 | | - > |
293 | | - <Download class="h-3 w-3" /> |
294 | | - </Button> |
295 | | - <Button |
296 | | - size="sm" |
297 | | - variant="ghost" |
298 | | - onclick={clearLogs} |
299 | | - disabled={logEntries.length === 0} |
300 | | - class="h-7 w-7 p-0 text-zinc-400 hover:text-white" |
301 | | - > |
302 | | - <Trash2 class="h-3 w-3" /> |
303 | | - </Button> |
| 291 | + <Tooltip.Root> |
| 292 | + <Tooltip.Trigger> |
| 293 | + <Button |
| 294 | + size="sm" |
| 295 | + variant="ghost" |
| 296 | + onclick={fetchLogs} |
| 297 | + disabled={loading} |
| 298 | + class="h-7 w-7 p-0 text-zinc-400 hover:text-white" |
| 299 | + > |
| 300 | + {#if loading} |
| 301 | + <Loader2 class="h-3 w-3 animate-spin" /> |
| 302 | + {:else} |
| 303 | + <RefreshCw class="h-3 w-3" /> |
| 304 | + {/if} |
| 305 | + </Button> |
| 306 | + </Tooltip.Trigger> |
| 307 | + <Tooltip.Content>Refresh logs</Tooltip.Content> |
| 308 | + </Tooltip.Root> |
| 309 | + <Tooltip.Root> |
| 310 | + <Tooltip.Trigger> |
| 311 | + <Button |
| 312 | + size="sm" |
| 313 | + variant="ghost" |
| 314 | + onclick={uploadToMCLogs} |
| 315 | + disabled={uploading} |
| 316 | + class="h-7 w-7 p-0 text-zinc-400 hover:text-white" |
| 317 | + > |
| 318 | + {#if uploading} |
| 319 | + <Loader2 class="h-3 w-3 animate-spin" /> |
| 320 | + {:else} |
| 321 | + <Upload class="h-3 w-3" /> |
| 322 | + {/if} |
| 323 | + </Button> |
| 324 | + </Tooltip.Trigger> |
| 325 | + <Tooltip.Content>Upload to mclo.gs</Tooltip.Content> |
| 326 | + </Tooltip.Root> |
| 327 | + <Tooltip.Root> |
| 328 | + <Tooltip.Trigger> |
| 329 | + <Button |
| 330 | + size="sm" |
| 331 | + variant="ghost" |
| 332 | + onclick={downloadLogs} |
| 333 | + disabled={logEntries.length === 0} |
| 334 | + class="h-7 w-7 p-0 text-zinc-400 hover:text-white" |
| 335 | + > |
| 336 | + <Download class="h-3 w-3" /> |
| 337 | + </Button> |
| 338 | + </Tooltip.Trigger> |
| 339 | + <Tooltip.Content>Download logs</Tooltip.Content> |
| 340 | + </Tooltip.Root> |
| 341 | + <Tooltip.Root> |
| 342 | + <Tooltip.Trigger> |
| 343 | + <Button |
| 344 | + size="sm" |
| 345 | + variant="ghost" |
| 346 | + onclick={clearLogs} |
| 347 | + disabled={logEntries.length === 0} |
| 348 | + class="h-7 w-7 p-0 text-zinc-400 hover:text-white" |
| 349 | + > |
| 350 | + <Trash2 class="h-3 w-3" /> |
| 351 | + </Button> |
| 352 | + </Tooltip.Trigger> |
| 353 | + <Tooltip.Content>Clear console</Tooltip.Content> |
| 354 | + </Tooltip.Root> |
304 | 355 | </div> |
305 | 356 | </div> |
306 | 357 | <div |
|
314 | 365 | No logs available. {[ServerStatus.RUNNING, ServerStatus.STARTING, ServerStatus.UNHEALTHY].includes(server.status) ? 'Try refreshing the page.' : 'Start the server to see output.'} |
315 | 366 | </div> |
316 | 367 | {:else} |
317 | | - {#each logEntries as entry} |
| 368 | + {#each logEntries as entry, i (i)} |
318 | 369 | <div class="log-line whitespace-pre-wrap break-all" data-type={entry.level}> |
| 370 | + <!-- eslint-disable-next-line svelte/no-at-html-tags --> |
319 | 371 | {@html ansiConverter.toHtml(entry.message)} |
320 | 372 | </div> |
321 | 373 | {/each} |
|
0 commit comments