Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/client-web/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@ const sidebarExpanded = ref(false)
</style>

<style>
.info-base-inspector-surface {
border: 1px solid var(--sys-color-border-base);
box-shadow: 0 3px 12px rgb(0 0 0 / 16%);
}

body {
margin: 0;
font-family: var(--sys-typo-family-sans);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
InkButton,
InkDialog,
InkDropdown,
InkJsonEditor,
InkLoading,
type JsonEditorValidation,
} from '@inkcre/ui-web'
Expand All @@ -18,6 +17,7 @@ import {
} from '@inkcre/extension-runtime-client-web'
import { extensionCardProps, extensionCardEmits } from './extensionCard'
import { extensionPeerControlMode, setExtensionPeerEnabled } from '@/extension-peer-control'
import SchemaConfigEditor from '@/components/schemaConfigEditor/schemaConfigEditor.vue'

const props = defineProps(extensionCardProps)
const emit = defineEmits(extensionCardEmits)
Expand All @@ -28,6 +28,11 @@ const configPopupOpen = ref(false)
const configSaving = ref(false)
const configValidation = ref<JsonEditorValidation>()
const versionPopupOpen = ref<boolean | Promise<boolean>>(false)
const managementOpen = ref(false)
function openManagement(): void {
operationError.value = null
managementOpen.value = true
}
const setupPopupOpen = ref(false)
const setupComponent = shallowRef<Component | null>(null)
const setupContribution = shallowRef(getExtensionSetupContribution(props.extension.name))
Expand All @@ -44,7 +49,6 @@ const versionOptions = ref<{ label: string; value: string; description?: string
const versionLoading = ref(false)
const versionError = ref<string | null>(null)
const documentation = shallowRef<ExtensionDocumentationLink[]>([])
const documentationOpen = ref(false)
const documentationRetry = ref(0)
const documentationStatus = ref<'loading' | 'available' | 'missing' | 'unavailable'>('loading')
const canSaveConfig = computed(
Expand All @@ -54,15 +58,8 @@ const canSaveConfig = computed(

// --- computed ---
watch(
() =>
[
props.extension.name,
props.extension.version,
documentationOpen.value,
documentationRetry.value,
] as const,
async ([name, version, open], _previous, onCleanup) => {
if (!open) return
() => [props.extension.name, props.extension.version, documentationRetry.value] as const,
async ([name, version], _previous, onCleanup) => {
let current = true
onCleanup(() => {
current = false
Expand Down Expand Up @@ -182,6 +179,7 @@ const onSetupClick = () => {
}

const onChangeVersionClick = async () => {
managementOpen.value = false
versionModel.value = props.extension.version
versionOptions.value = []
versionError.value = null
Expand Down Expand Up @@ -250,6 +248,7 @@ const onUninstall = async () => {
isUninstalling.value = true
try {
await getExtensionHost().uninstall(props.extension.name)
managementOpen.value = false
emit('uninstalled')
} catch (error) {
operationError.value = error instanceof Error ? error.message : String(error)
Expand Down Expand Up @@ -298,12 +297,24 @@ const onUninstall = async () => {
@click="onSetupClick"
/>
<InkButton @click="onEditConfigClick" :text="t('extension.editConfig')" size="sm" />
<InkButton
:text="t('extension.versionAndRemoval')"
size="sm"
theme="subtle"
@click="openManagement"
/>
</div>
<p class="extension-card__hint">
{{ t('extension.enabledPeerCount', { count: extension.enabled.length }) }}
</p>
<details class="extension-card__details">
<summary>{{ t('extension.versionAndRemoval') }}</summary>
<InkDialog
v-model="managementOpen"
:title="t('extension.versionAndRemoval')"
:subtitle="`${extension.nickname ?? extension.name} · v${extension.version}`"
:show-confirm="false"
:cancel-text="t('common.close')"
:is-loading="isUninstalling"
>
<div class="extension-card__actions">
<InkButton
@click="onChangeVersionClick"
Expand All @@ -324,13 +335,10 @@ const onUninstall = async () => {
<p v-if="extension.enabled.length > 0" class="extension-card__hint">
{{ t('extension.uninstallDisabled') }}
</p>
</details>
<p v-if="operationError" role="alert" class="extension-card__error">{{ operationError }}</p>
<details
class="extension-card__details"
@toggle="documentationOpen = ($event.target as HTMLDetailsElement).open"
>
<summary>{{ t('extension.documentation') }}</summary>
<p v-else class="extension-card__hint">{{ t('extension.uninstallConfirm') }}</p>
<p v-if="operationError" role="alert" class="extension-card__error">{{ operationError }}</p>
</InkDialog>
<div class="extension-card__documentation">
<InkLoading
v-if="documentationStatus === 'loading'"
variant="spinner"
Expand All @@ -350,6 +358,7 @@ const onUninstall = async () => {
rel="noopener noreferrer"
>
{{ t(`extension.documentationScope.${link.scope}`) }}
<span aria-hidden="true"> ↗</span>
</a>
</nav>
<p v-else-if="documentationStatus === 'unavailable'" class="extension-card__hint">
Expand All @@ -364,7 +373,7 @@ const onUninstall = async () => {
size="sm"
@click="documentationRetry++"
/>
</details>
</div>

<InkDialog
v-model="peerDialogOpen"
Expand Down Expand Up @@ -440,8 +449,9 @@ const onUninstall = async () => {
v-model="configPopupOpen"
:title="t('extension.editConfigTitle')"
:is-loading="configSaving"
style="width: min(48rem, calc(100vw - 2 * var(--sys-space-md)))"
>
<InkJsonEditor
<SchemaConfigEditor
v-model="configModel"
:schema="extension.config_schema ?? undefined"
:label="t('extension.editConfigTitle')"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ async function ruminate(): Promise<void> {

<template>
<InkPopup
class="info-base-inspector-surface"
style="width: 420px; max-width: calc(100vw - 2 * var(--sys-space-md)); padding: 0"
:open="open"
:scrim="false"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ function close(): void {

<template>
<InkPopup
class="info-base-inspector-surface"
style="width: 360px; max-width: calc(100vw - 2 * var(--sys-space-md))"
:open="true"
:scrim="false"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ function onOpenChange(value: boolean): void {

<template>
<InkPopup
class="info-base-inspector-surface"
style="width: 900px; max-width: calc(100vw - 2 * var(--sys-space-md)); padding: 0"
:open="open"
:scrim="false"
Expand Down
5 changes: 2 additions & 3 deletions apps/client-web/src/components/obsrv/LogEntry/LogEntry.scss
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,13 @@
color: sys-var(color, text, base);
flex: 1 1 12rem;
min-width: 0;
word-break: break-all;
overflow-x: scroll;
overflow-wrap: anywhere;

&--code {
padding: sys-var(space, xs);
border-radius: sys-var(radius, none);
margin: 0;
white-space: pre;
white-space: pre-wrap;
}
}
}
18 changes: 14 additions & 4 deletions apps/client-web/src/components/peer/peerCard/peerCard.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
justify-content: space-between;
align-items: center;
padding: sys-var(space, md);
border: 1px solid sys-var(color, border, base);
background-color: sys-var(color, surface, base);
background-color: sys-var(color, surface, subtle);

&__item-info {
min-width: 0;
Expand All @@ -31,8 +30,7 @@
}

&__current {
padding-inline-start: sys-var(space, sm);
border-inline-start: 1px solid sys-var(color, border, base);
white-space: nowrap;
}

&__item-id {
Expand All @@ -51,6 +49,18 @@
gap: sys-var(space, xs);
}

&__readiness {
@include apply-font(body-sm);
max-width: 100%;
p {
margin: sys-var(space, xs) 0;
}
ul {
margin: sys-var(space, xs) 0;
padding-left: sys-var(space, md);
}
}

&__details {
@include apply-font(body-sm);
max-width: 100%;
Expand Down
3 changes: 2 additions & 1 deletion apps/client-web/src/components/peer/peerCard/peerCard.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { type PropType } from 'vue'
import { Peer } from '@inkcre/core'
import { Peer, type PeerReadinessObservation } from '@inkcre/core'

export const peerCardProps = {
peer: { type: Object as PropType<Peer>, required: true },
current: { type: Boolean, default: false },
readiness: { type: Object as PropType<PeerReadinessObservation>, default: undefined },
status: {
type: String as PropType<'online' | 'offline' | 'unknown'>,
required: true,
Expand Down
42 changes: 38 additions & 4 deletions apps/client-web/src/components/peer/peerCard/peerCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@ const configError = ref('')
const deleteOpen = ref(false)
const deleting = ref(false)
const deleteError = ref('')
const diagnostics = computed(() =>
props.readiness && props.readiness.status !== 'unavailable' ? props.readiness.diagnostics : null
)
const databaseComponents = computed(() =>
diagnostics.value
? Object.entries(diagnostics.value.database).filter(
(entry): entry is [string, { status: string; problems?: string[] }] =>
typeof entry[1] === 'object' &&
((entry[1].status !== 'ok' && entry[1].status !== 'not_required') ||
!!entry[1].problems?.length)
)
: []
)
function openDelete() {
deleteError.value = ''
deleteOpen.value = true
Expand Down Expand Up @@ -103,6 +116,9 @@ const getStatusText = (status: 'online' | 'offline' | 'unknown') => {
<div class="peer-card__item-info">
<div class="peer-card__heading">
<InkInput v-model="peer.name" type="inline" @confirm="savePeer" />
<span :class="['peer-card__item-status', `peer-card__item-status--${status}`]">
{{ getStatusText(status) }}
</span>
<span class="peer-card__version">
{{ peer.application_version ? `v${peer.application_version}` : t('peer.versionUnknown') }}
</span>
Expand All @@ -119,6 +135,28 @@ const getStatusText = (status: 'online' | 'offline' | 'unknown') => {
@click="openDelete"
/>
</div>
<section v-if="diagnostics" class="peer-card__readiness" :aria-label="t('peer.readiness')">
<p>
<strong>{{ t(`peer.readinessStatus.${diagnostics.status}`) }}</strong> ·
{{ t('peer.phase') }}: <code>{{ diagnostics.runtime.phase }}</code> ·
{{ t('peer.reason') }}: <code>{{ diagnostics.runtime.reason }}</code>
</p>
<p v-if="diagnostics.runtime.step">
{{ t('peer.step') }}: <code>{{ diagnostics.runtime.step }}</code>
</p>
<p>{{ t('peer.database') }}: {{ diagnostics.database.status }}</p>
<ul v-if="databaseComponents.length">
<li v-for="[name, component] in databaseComponents" :key="name">
<span>{{ name }}: {{ component.status }}</span>
<ul v-if="component.problems?.length">
<li v-for="problem in component.problems" :key="problem">{{ problem }}</li>
</ul>
</li>
</ul>
</section>
<p v-else-if="readiness?.status === 'unavailable'" class="peer-card__readiness">
{{ t(`peer.readinessUnavailable.${readiness.reason}`) }}
</p>
<details class="peer-card__details">
<summary>{{ t('common.details') }}</summary>
<code class="peer-card__item-id">{{ peer.id }}</code>
Expand All @@ -127,10 +165,6 @@ const getStatusText = (status: 'online' | 'offline' | 'unknown') => {
<p v-if="!current && status === 'online'">{{ t('peer.deleteOnline') }}</p>
</details>
</div>
<span :class="['peer-card__item-status', `peer-card__item-status--${status}`]">
{{ getStatusText(status) }}
</span>

<InkDialog v-model="configPopupOpen" :title="t('peer.editConfig')" :is-loading="savingConfig">
<SchemaConfigEditor
v-model="configModel"
Expand Down
11 changes: 7 additions & 4 deletions apps/client-web/src/components/peer/peerList/peerList.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@

&__header {
display: flex;
justify-content: flex-end;
justify-content: space-between;
align-items: center;
padding-bottom: sys-var(space, md);
border-bottom: 1px solid sys-var(color, border, base);
gap: sys-var(space, md);
h1 {
@include apply-font(title-lg);
margin: 0;
}
}

&__list {
display: flex;
flex-direction: column;
gap: sys-var(space, sm);
gap: sys-var(space, lg);
}
&__skeleton {
display: grid;
Expand Down
14 changes: 13 additions & 1 deletion apps/client-web/src/components/peer/peerList/peerList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,19 @@ import { useI18n } from 'vue-i18n'
import { InkButton, InkSkeleton, InkPlaceholder } from '@inkcre/ui-web'
import { RouterLink } from 'vue-router'
import { currentWebPeer } from '@/core'
import { configStore, Peer, PeerManager } from '@inkcre/core'
import {
configStore,
Peer,
PeerManager,
probePeerReadiness,
type PeerReadinessObservation,
} from '@inkcre/core'
import PeerCard from '../peerCard/peerCard.vue'

const { t } = useI18n()
const peers = ref<Peer[]>([])
const livePeers = ref(new Set<string>())
const readiness = ref(new Map<string, PeerReadinessObservation>())
const loading = ref(false)
const error = ref('')
const currentPeerId = configStore.metaConfig.INKCRE_PEER_ID
Expand All @@ -27,6 +34,9 @@ const refreshPeers = async () => {
livePeers.value = new Set(live.map((peer) => peer.id))
const self = all.find((peer) => peer.id === currentPeerId)
if (self) currentWebPeer.value = { id: self.id, name: self.name }
readiness.value = new Map(
await Promise.all(all.map(async (peer) => [peer.id, await probePeerReadiness(peer)] as const))
)
} catch (cause) {
error.value = cause instanceof Error ? cause.message : String(cause)
} finally {
Expand All @@ -45,6 +55,7 @@ onMounted(refreshPeers)
<template>
<section class="peer-list">
<div v-if="connected" class="peer-list__header">
<h1>{{ t('peer.title') }}</h1>
<InkButton :text="t('peer.refresh')" size="sm" :is-loading="loading" @click="refreshPeers" />
</div>

Expand Down Expand Up @@ -81,6 +92,7 @@ onMounted(refreshPeers)
:peer="peer"
:status="getPeerStatus(peer)"
:current="peer.id === currentPeerId"
:readiness="readiness.get(peer.id)"
@updated="refreshPeers"
/>
</div>
Expand Down
Loading
Loading