mirror of
https://github.com/aljazceru/nostr-watch.git
synced 2025-12-18 05:44:25 +01:00
285 lines
7.4 KiB
Vue
285 lines
7.4 KiB
Vue
<template>
|
||
<td class="status-indicator" :key="generateKey(relay, 'aggregate')">
|
||
<span :class="result?.aggregate" class="aggregate indicator">
|
||
<span></span>
|
||
<span></span>
|
||
</span>
|
||
</td>
|
||
|
||
<td class="relay left-align relay-url">
|
||
<router-link :to="`/relay/${relayClean(relay)}`" active-class="active">{{ relay }}</router-link>
|
||
</td>
|
||
|
||
<td class="verified">
|
||
<span v-if="result?.identities">
|
||
<span v-tooltip:top.tooltip="identityList()"> <span class="verified-shape-wrapper" v-if="Object.entries(result?.identities).length"><span class="shape verified"></span></span></span>
|
||
</span>
|
||
</td>
|
||
|
||
<td class="location">{{ getFlag() }}</td>
|
||
|
||
<td class="latency">
|
||
<span>{{ result?.latency.final }}<span v-if="result?.check.latency">ms</span></span>
|
||
</td>
|
||
|
||
<td class="connect" :key="generateKey(relay, 'check.connect')">
|
||
<span :class="getResultClass(relay, 'connect')"></span>
|
||
</td>
|
||
|
||
<td class="read" :key="generateKey(relay, 'check.read')">
|
||
<span :class="getResultClass(relay, 'read')"></span>
|
||
</td>
|
||
|
||
<td class="write" :key="generateKey(relay, 'check.write')">
|
||
<span :class="getResultClass(relay, 'write')"></span>
|
||
</td>
|
||
|
||
<td class="info">
|
||
<ul v-if="result?.observations && result?.observations.length">
|
||
<li class="observation" v-for="(alert) in result?.observations" :key="generateKey(relay, alert.description)">
|
||
<span v-tooltip:top.tooltip="alert.description" :class="alert.type" v-if="alert.type == 'notice'">✉️</span>
|
||
<span v-tooltip:top.tooltip="alert.description" :class="alert.type" v-if="alert.type == 'caution'">⚠️</span>
|
||
</li>
|
||
</ul>
|
||
</td>
|
||
|
||
<vue-final-modal v-model="showModal" classes="modal-container" content-class="modal-content">
|
||
<div class="modal__title">
|
||
<span>{{ result?.info?.name }}</span>
|
||
</div>
|
||
<div class="modal__content">
|
||
<div v-if="result?.info?.description">
|
||
{{ result?.info?.description }} <br/>
|
||
<strong v-if="result?.info?.pubkey">Public Key:</strong> {{ result?.info?.pubkey }} <br/>
|
||
<strong v-if="result?.info?.contact">Contact:</strong> <SafeMail :email="result?.info?.contact" v-if="result?.info?.contact" />
|
||
</div>
|
||
<div>
|
||
<h4>Status</h4>
|
||
<ul>
|
||
<li><strong>Connected</strong> <span :class="getResultClass(relay, 'connect')" class="connect indicator"></span></li>
|
||
<li><strong>Read</strong> <span :class="getResultClass(relay, 'read')" class="read indicator"></span></li>
|
||
<li><strong>Write</strong> <span :class="getResultClass(relay, 'write')" class="write indicator"></span></li>
|
||
</ul>
|
||
</div>
|
||
<h4>Relay Info</h4>
|
||
<ul>
|
||
<li><strong>Software:</strong> {{ result?.info?.software }} </li>
|
||
<li><strong>Version</strong>: {{ result?.info?.version }} </li>
|
||
</ul>
|
||
<h4>NIP Support</h4>
|
||
<ul>
|
||
<li v-for="(nip) in result?.info?.supported_nips" :key="`${relay}_${nip}`">
|
||
<a :href="nipLink(nip)" target="_blank">{{ nipFormatted(nip) }}</a>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</vue-final-modal>
|
||
</template>
|
||
|
||
<script>
|
||
import { defineComponent} from 'vue'
|
||
import { VueFinalModal } from 'vue-final-modal'
|
||
import { InspectorRelayResult } from 'nostr-relay-inspector'
|
||
import SafeMail from "@2alheure/vue-safe-mail";
|
||
import { countryCodeEmoji } from 'country-code-emoji';
|
||
import emoji from 'node-emoji';
|
||
|
||
export default defineComponent({
|
||
name: 'RelaySingleComponent',
|
||
components: {
|
||
VueFinalModal,
|
||
SafeMail
|
||
},
|
||
props: {
|
||
relay: String,
|
||
result: {
|
||
type: Object,
|
||
default(){
|
||
return structuredClone(InspectorRelayResult)
|
||
}
|
||
},
|
||
geo: {
|
||
type: Object,
|
||
default(){
|
||
return {}
|
||
}
|
||
},
|
||
showColumns: {
|
||
type: Object,
|
||
default() {
|
||
return {
|
||
connectionStatuses: false,
|
||
nips: false,
|
||
geo: false,
|
||
additionalInfo: false
|
||
}
|
||
}
|
||
},
|
||
connection: {
|
||
type: Object,
|
||
default() {
|
||
return {
|
||
connectionStatuses: false,
|
||
nips: false,
|
||
geo: false,
|
||
additionalInfo: false
|
||
}
|
||
}
|
||
}
|
||
},
|
||
data() {
|
||
return {
|
||
showModal: false
|
||
}
|
||
},
|
||
methods: {
|
||
getResultClass (url, key) {
|
||
let result = this.result?.check?.[key] === true
|
||
? 'success'
|
||
: this.result?.check?.[key] === false
|
||
? 'failure'
|
||
: 'pending'
|
||
return `indicator ${result}`
|
||
},
|
||
getLoadingClass () {
|
||
return this.result?.state == 'complete' ? "relay loaded" : "relay"
|
||
},
|
||
generateKey (url, key) {
|
||
return `${url}_${key}`
|
||
},
|
||
|
||
getFlag () {
|
||
return this.geo?.countryCode ? countryCodeEmoji(this.geo.countryCode) : emoji.get('shrug');
|
||
},
|
||
|
||
setCheck (bool) {
|
||
return bool ? '✅ ' : ''
|
||
},
|
||
|
||
setCross (bool) {
|
||
return !bool ? '❌' : ''
|
||
},
|
||
setCaution (bool) {
|
||
return !bool ? '⚠️' : ''
|
||
},
|
||
identityList () {
|
||
let string = '',
|
||
extraString = '',
|
||
users = Object.entries(this.result?.identities),
|
||
count = 0
|
||
|
||
if(this.result?.identities) {
|
||
if(this.result?.identities.serverAdmin) {
|
||
string = `Relay has registered an administrator pubkey: ${this.result?.identities.serverAdmin}. `
|
||
extraString = "Additionally, "
|
||
}
|
||
|
||
const total = users.filter(([key]) => key!='serverAdmin').length,
|
||
isOne = total==1
|
||
|
||
if(total) {
|
||
string = `${string}${extraString}Relay domain contains NIP-05 verification data for:`
|
||
users.forEach( ([key]) => {
|
||
if(key == "serverAdmin") return
|
||
count++
|
||
string = `${string} ${(count==total && !isOne) ? 'and' : ''} @${key}${(count!=total && !isOne) ? ', ' : ''}`
|
||
})
|
||
}
|
||
}
|
||
return string
|
||
},
|
||
relayClean(relay) {
|
||
return relay.replace('wss://', '')
|
||
},
|
||
nipSignature(key){
|
||
return key.toString().length == 1 ? `0${key}` : key
|
||
},
|
||
nipFormatted(key){
|
||
return `NIP-${this.nipSignature(key)}`
|
||
},
|
||
nipLink(key){
|
||
return `https://github.com/nostr-protocol/nips/blob/master/${this.nipSignature(key)}.md`
|
||
},
|
||
async copy(text) {
|
||
try {
|
||
await navigator.clipboard.writeText(text);
|
||
} catch(err) {
|
||
console.error(err)
|
||
}
|
||
},
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
ul {
|
||
margin:0;
|
||
padding:0;
|
||
}
|
||
|
||
li {
|
||
margin:0;
|
||
padding:0;
|
||
list-style:none;
|
||
}
|
||
|
||
td.nip-11,
|
||
td.verified span {
|
||
cursor: pointer
|
||
}
|
||
|
||
::v-deep(.modal-container) {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
::v-deep(.modal-content) {
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
max-height: 90%;
|
||
margin: 0 1rem;
|
||
padding: 1rem;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 0.25rem;
|
||
background: #fff;
|
||
}
|
||
.modal__title {
|
||
margin: 0 2rem 0 0;
|
||
font-size: 1.5rem;
|
||
font-weight: 700;
|
||
}
|
||
.modal__content {
|
||
flex-grow: 1;
|
||
overflow-y: auto;
|
||
}
|
||
.modal__action {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
padding: 1rem 0 0;
|
||
}
|
||
.modal__close {
|
||
position: absolute;
|
||
top: 0.5rem;
|
||
right: 0.5rem;
|
||
}
|
||
|
||
|
||
.dark-mode div ::v-deep(.modal-content) {
|
||
border-color: #2d3748;
|
||
background-color: #1a202c;
|
||
}
|
||
|
||
.restricted.aggregate.indicator {
|
||
position:relative;
|
||
left:-7px;
|
||
}
|
||
|
||
td {
|
||
padding-top:2px;
|
||
padding-bottom:2px;
|
||
}
|
||
</style>
|