@le-space/ui 0.7.0 → 0.8.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/package.json +3 -3
- package/react/index.d.ts +51 -0
- package/react/index.js +601 -128
- package/shared/index.d.ts +61 -2
- package/shared/index.js +172 -4
- package/styles.css +6 -0
- package/svelte/SponsorRelayFab.svelte +284 -0
- package/svelte/styles/theme.css +6 -0
|
@@ -91,6 +91,44 @@ export let apiHosts = undefined
|
|
|
91
91
|
)
|
|
92
92
|
$: orphanRegistrations = state.orphanBootstrapRegistrations ?? []
|
|
93
93
|
|
|
94
|
+
// Which tab each expanded deployment card is showing, keyed by item hash.
|
|
95
|
+
let instanceTabs = {}
|
|
96
|
+
|
|
97
|
+
function instanceTab(itemHash) {
|
|
98
|
+
return instanceTabs[itemHash] ?? 'general'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function selectInstanceTab(itemHash, tab) {
|
|
102
|
+
instanceTabs = { ...instanceTabs, [itemHash]: tab }
|
|
103
|
+
if (tab !== 'general') {
|
|
104
|
+
// Addresses are live state — AutoTLS entries turn up a minute or two after
|
|
105
|
+
// the deploy reports success — so load on open and let the user refresh.
|
|
106
|
+
controller.loadInstanceAddresses(itemHash)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function addressesOf(itemHash) {
|
|
111
|
+
return state.instanceAddresses?.[itemHash] ?? null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Label the tab by what the deployment actually publishes, not by profile. */
|
|
115
|
+
function addressTabLabel(itemHash) {
|
|
116
|
+
const addresses = addressesOf(itemHash)
|
|
117
|
+
if (addresses?.groups?.length) return 'Multiaddresses'
|
|
118
|
+
if (addresses?.pwaEnv) return 'Endpoints'
|
|
119
|
+
return 'Multiaddresses'
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function copyAllText(addresses) {
|
|
123
|
+
return (addresses?.groups ?? []).flatMap((group) => group.addresses).join('\n')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function pwaEnvText(addresses) {
|
|
127
|
+
return Object.entries(addresses?.pwaEnv ?? {})
|
|
128
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
129
|
+
.join('\n')
|
|
130
|
+
}
|
|
131
|
+
|
|
94
132
|
onMount(async () => {
|
|
95
133
|
const unsubscribe = controller.subscribe((next) => {
|
|
96
134
|
state = next
|
|
@@ -359,6 +397,38 @@ export let apiHosts = undefined
|
|
|
359
397
|
</button>
|
|
360
398
|
</div>
|
|
361
399
|
|
|
400
|
+
{@const itemHash = entry.instance.item_hash}
|
|
401
|
+
{@const addresses = addressesOf(itemHash)}
|
|
402
|
+
{@const tab = instanceTab(itemHash)}
|
|
403
|
+
|
|
404
|
+
<div class="tab-bar" role="tablist">
|
|
405
|
+
<button
|
|
406
|
+
type="button"
|
|
407
|
+
role="tab"
|
|
408
|
+
class="tab"
|
|
409
|
+
class:tab-active={tab === 'general'}
|
|
410
|
+
aria-selected={tab === 'general'}
|
|
411
|
+
on:click={() => selectInstanceTab(itemHash, 'general')}
|
|
412
|
+
>General</button>
|
|
413
|
+
<button
|
|
414
|
+
type="button"
|
|
415
|
+
role="tab"
|
|
416
|
+
class="tab"
|
|
417
|
+
class:tab-active={tab === 'addresses'}
|
|
418
|
+
aria-selected={tab === 'addresses'}
|
|
419
|
+
on:click={() => selectInstanceTab(itemHash, 'addresses')}
|
|
420
|
+
>{addressTabLabel(itemHash)}</button>
|
|
421
|
+
<button
|
|
422
|
+
type="button"
|
|
423
|
+
role="tab"
|
|
424
|
+
class="tab"
|
|
425
|
+
class:tab-active={tab === 'health'}
|
|
426
|
+
aria-selected={tab === 'health'}
|
|
427
|
+
on:click={() => selectInstanceTab(itemHash, 'health')}
|
|
428
|
+
>Health</button>
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
{#if tab === 'general'}
|
|
362
432
|
<div class="instance-grid">
|
|
363
433
|
<div>
|
|
364
434
|
<span>Host IPv4</span>
|
|
@@ -389,12 +459,109 @@ export let apiHosts = undefined
|
|
|
389
459
|
<strong>{joinMappedPorts(entry.details.mappedPorts)}</strong>
|
|
390
460
|
</div>
|
|
391
461
|
|
|
462
|
+
{#if entry.details.proxyHostname}
|
|
463
|
+
<div class="mono-block">
|
|
464
|
+
<span>Proxy hostname</span>
|
|
465
|
+
<strong>{entry.details.proxyHostname}</strong>
|
|
466
|
+
<CopyButton text={entry.details.proxyHostname} />
|
|
467
|
+
</div>
|
|
468
|
+
{/if}
|
|
469
|
+
|
|
392
470
|
{#if bootstrapUiEnabled() && confirmedRegistration}
|
|
393
471
|
<div class="mono-block">
|
|
394
472
|
<span>Bootstrap Registration</span>
|
|
395
473
|
<strong>{shortHash(confirmedRegistration.messageHash ?? confirmedRegistration.content?.registrationId ?? 'confirmed', 14, 8)}</strong>
|
|
396
474
|
</div>
|
|
397
475
|
{/if}
|
|
476
|
+
{/if}
|
|
477
|
+
|
|
478
|
+
{#if tab === 'addresses'}
|
|
479
|
+
<div class="tab-head">
|
|
480
|
+
<span>{addresses?.sourceUrl ?? 'Reading from the deployment'}</span>
|
|
481
|
+
<button
|
|
482
|
+
type="button"
|
|
483
|
+
class="refresh"
|
|
484
|
+
disabled={addresses?.status === 'loading'}
|
|
485
|
+
on:click={() => controller.loadInstanceAddresses(itemHash, { force: true })}
|
|
486
|
+
>{addresses?.status === 'loading' ? 'Loading…' : 'Refresh'}</button>
|
|
487
|
+
</div>
|
|
488
|
+
|
|
489
|
+
{#if addresses?.status === 'loading' && !addresses?.groups?.length}
|
|
490
|
+
<p class="tab-note">Reading the deployment's address document…</p>
|
|
491
|
+
{:else if addresses?.status === 'unsupported'}
|
|
492
|
+
<p class="tab-note">{addresses.error}</p>
|
|
493
|
+
{:else if addresses?.status === 'error'}
|
|
494
|
+
<p class="alert error">{addresses.error}</p>
|
|
495
|
+
{:else if addresses?.groups?.length}
|
|
496
|
+
{#if addresses.peerId}
|
|
497
|
+
<div class="mono-block">
|
|
498
|
+
<span>Peer ID</span>
|
|
499
|
+
<strong>{addresses.peerId}</strong>
|
|
500
|
+
<CopyButton text={addresses.peerId} />
|
|
501
|
+
</div>
|
|
502
|
+
{/if}
|
|
503
|
+
|
|
504
|
+
{#each addresses.groups as group}
|
|
505
|
+
<div class="address-group">
|
|
506
|
+
<div class="address-group-head">
|
|
507
|
+
<strong>{group.label}</strong>
|
|
508
|
+
<CopyButton text={group.addresses.join('\n')} label="Copy group" />
|
|
509
|
+
</div>
|
|
510
|
+
{#each group.addresses as address}
|
|
511
|
+
<div class="address-row">
|
|
512
|
+
<code>{address}</code>
|
|
513
|
+
<CopyButton text={address} />
|
|
514
|
+
</div>
|
|
515
|
+
{/each}
|
|
516
|
+
</div>
|
|
517
|
+
{/each}
|
|
518
|
+
|
|
519
|
+
<div class="link-row">
|
|
520
|
+
<CopyButton text={copyAllText(addresses)} label="Copy all" />
|
|
521
|
+
<CopyButton text={JSON.stringify(addresses.payload, null, 2)} label="Copy JSON" />
|
|
522
|
+
</div>
|
|
523
|
+
{:else if addresses?.pwaEnv}
|
|
524
|
+
<div class="address-group">
|
|
525
|
+
<div class="address-group-head">
|
|
526
|
+
<strong>PWA environment</strong>
|
|
527
|
+
<CopyButton text={pwaEnvText(addresses)} label="Copy block" />
|
|
528
|
+
</div>
|
|
529
|
+
{#each Object.entries(addresses.pwaEnv) as [key, value]}
|
|
530
|
+
<div class="address-row">
|
|
531
|
+
<code>{key}={value}</code>
|
|
532
|
+
<CopyButton text={`${key}=${value}`} />
|
|
533
|
+
</div>
|
|
534
|
+
{/each}
|
|
535
|
+
</div>
|
|
536
|
+
{:else}
|
|
537
|
+
<p class="tab-note">This deployment reports no addresses yet.</p>
|
|
538
|
+
{/if}
|
|
539
|
+
{/if}
|
|
540
|
+
|
|
541
|
+
{#if tab === 'health'}
|
|
542
|
+
<div class="tab-head">
|
|
543
|
+
<span>{addresses?.sourceUrl ?? 'Reading from the deployment'}</span>
|
|
544
|
+
<button
|
|
545
|
+
type="button"
|
|
546
|
+
class="refresh"
|
|
547
|
+
disabled={addresses?.status === 'loading'}
|
|
548
|
+
on:click={() => controller.loadInstanceAddresses(itemHash, { force: true })}
|
|
549
|
+
>{addresses?.status === 'loading' ? 'Loading…' : 'Refresh'}</button>
|
|
550
|
+
</div>
|
|
551
|
+
|
|
552
|
+
{#if addresses?.payload}
|
|
553
|
+
<pre class="payload">{JSON.stringify(addresses.payload, null, 2)}</pre>
|
|
554
|
+
<div class="link-row">
|
|
555
|
+
<CopyButton text={JSON.stringify(addresses.payload, null, 2)} label="Copy JSON" />
|
|
556
|
+
</div>
|
|
557
|
+
{:else if addresses?.status === 'loading'}
|
|
558
|
+
<p class="tab-note">Reading the deployment's health document…</p>
|
|
559
|
+
{:else if addresses?.error}
|
|
560
|
+
<p class="tab-note">{addresses.error}</p>
|
|
561
|
+
{:else}
|
|
562
|
+
<p class="tab-note">No health document read yet.</p>
|
|
563
|
+
{/if}
|
|
564
|
+
{/if}
|
|
398
565
|
|
|
399
566
|
<div class="link-row">
|
|
400
567
|
<CopyButton text={entry.instance.item_hash} label="Copy hash" />
|
|
@@ -800,6 +967,14 @@ export let apiHosts = undefined
|
|
|
800
967
|
font: 500 0.8125rem/1.4 var(--relay-font-body);
|
|
801
968
|
}
|
|
802
969
|
|
|
970
|
+
/* The list, not the closed control. Without this it inherits the select's
|
|
971
|
+
translucent field background and keeps --relay-text, which on a light
|
|
972
|
+
drop-down is light-on-light. */
|
|
973
|
+
option {
|
|
974
|
+
background: var(--relay-menu-bg);
|
|
975
|
+
color: var(--relay-text);
|
|
976
|
+
}
|
|
977
|
+
|
|
803
978
|
input:focus-visible,
|
|
804
979
|
select:focus-visible,
|
|
805
980
|
textarea:focus-visible {
|
|
@@ -830,6 +1005,115 @@ export let apiHosts = undefined
|
|
|
830
1005
|
gap: 0.3rem;
|
|
831
1006
|
}
|
|
832
1007
|
|
|
1008
|
+
.tab-bar {
|
|
1009
|
+
display: flex;
|
|
1010
|
+
gap: 0.35rem;
|
|
1011
|
+
margin: 0.75rem 0 0.5rem;
|
|
1012
|
+
border-bottom: 1px solid var(--relay-surface-border);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
.tab {
|
|
1016
|
+
border: 0;
|
|
1017
|
+
border-bottom: 2px solid transparent;
|
|
1018
|
+
background: transparent;
|
|
1019
|
+
color: var(--relay-muted);
|
|
1020
|
+
padding: 0.35rem 0.6rem;
|
|
1021
|
+
font: 700 0.6875rem/1 var(--relay-font-mono);
|
|
1022
|
+
text-transform: uppercase;
|
|
1023
|
+
letter-spacing: 0.06em;
|
|
1024
|
+
cursor: pointer;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
.tab:hover {
|
|
1028
|
+
color: var(--relay-text);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
.tab-active {
|
|
1032
|
+
color: var(--relay-text);
|
|
1033
|
+
border-bottom-color: var(--relay-cyan);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
.tab-head {
|
|
1037
|
+
display: flex;
|
|
1038
|
+
align-items: center;
|
|
1039
|
+
justify-content: space-between;
|
|
1040
|
+
gap: 0.5rem;
|
|
1041
|
+
margin-bottom: 0.5rem;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
.tab-head span {
|
|
1045
|
+
color: var(--relay-muted);
|
|
1046
|
+
font: 400 0.6875rem/1.4 var(--relay-font-mono);
|
|
1047
|
+
overflow-wrap: anywhere;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
.refresh {
|
|
1051
|
+
border: 1px solid var(--relay-surface-border);
|
|
1052
|
+
border-radius: 999px;
|
|
1053
|
+
background: transparent;
|
|
1054
|
+
color: var(--relay-muted);
|
|
1055
|
+
padding: 0.28rem 0.7rem;
|
|
1056
|
+
font: 700 0.6563rem/1 var(--relay-font-mono);
|
|
1057
|
+
text-transform: uppercase;
|
|
1058
|
+
letter-spacing: 0.06em;
|
|
1059
|
+
cursor: pointer;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
.refresh:hover:not(:disabled) {
|
|
1063
|
+
color: var(--relay-text);
|
|
1064
|
+
border-color: var(--relay-cyan);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
.refresh:disabled {
|
|
1068
|
+
opacity: 0.6;
|
|
1069
|
+
cursor: default;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
.tab-note {
|
|
1073
|
+
color: var(--relay-muted);
|
|
1074
|
+
font-size: 0.75rem;
|
|
1075
|
+
margin: 0.5rem 0;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
.address-group {
|
|
1079
|
+
margin-bottom: 0.75rem;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
.address-group-head {
|
|
1083
|
+
display: flex;
|
|
1084
|
+
align-items: center;
|
|
1085
|
+
justify-content: space-between;
|
|
1086
|
+
gap: 0.5rem;
|
|
1087
|
+
margin-bottom: 0.3rem;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
.address-group-head strong {
|
|
1091
|
+
font-size: 0.75rem;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
.address-row {
|
|
1095
|
+
display: flex;
|
|
1096
|
+
align-items: center;
|
|
1097
|
+
justify-content: space-between;
|
|
1098
|
+
gap: 0.5rem;
|
|
1099
|
+
padding: 0.25rem 0;
|
|
1100
|
+
border-top: 1px solid var(--relay-surface-border);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
.address-row code {
|
|
1104
|
+
font: 400 0.6875rem/1.4 var(--relay-font-mono);
|
|
1105
|
+
overflow-wrap: anywhere;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
.payload {
|
|
1109
|
+
max-height: 20rem;
|
|
1110
|
+
overflow: auto;
|
|
1111
|
+
padding: 0.5rem;
|
|
1112
|
+
border: 1px solid var(--relay-surface-border);
|
|
1113
|
+
border-radius: 0.5rem;
|
|
1114
|
+
font: 400 0.6875rem/1.5 var(--relay-font-mono);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
833
1117
|
.deployment-box strong {
|
|
834
1118
|
font-family: var(--relay-font-mono);
|
|
835
1119
|
font-size: 0.75rem;
|
package/svelte/styles/theme.css
CHANGED
|
@@ -42,6 +42,11 @@
|
|
|
42
42
|
--relay-surface: rgba(11, 14, 21, 0.55);
|
|
43
43
|
--relay-surface-border: var(--relay-horizon);
|
|
44
44
|
--relay-field-bg: rgba(11, 14, 21, 0.85);
|
|
45
|
+
/* Opaque on purpose. A select's drop-down takes the control's own colours on
|
|
46
|
+
the platforms that honour author styling, and it opens over the page rather
|
|
47
|
+
than over the panel - a translucent background would composite against
|
|
48
|
+
whatever is behind it and leave light text on a light list. */
|
|
49
|
+
--relay-menu-bg: var(--relay-deep-space);
|
|
45
50
|
--relay-accent: var(--relay-coral);
|
|
46
51
|
--relay-accent-contrast: #0b0e15;
|
|
47
52
|
--relay-link: var(--relay-cyan);
|
|
@@ -81,6 +86,7 @@
|
|
|
81
86
|
--relay-surface: rgba(20, 27, 46, 0.04);
|
|
82
87
|
--relay-surface-border: #d9e0ec;
|
|
83
88
|
--relay-field-bg: #ffffff;
|
|
89
|
+
--relay-menu-bg: #ffffff;
|
|
84
90
|
--relay-accent: var(--relay-coral);
|
|
85
91
|
--relay-accent-contrast: #ffffff;
|
|
86
92
|
--relay-link: var(--relay-cyan);
|