124 lines
3.5 KiB
Vue
124 lines
3.5 KiB
Vue
<template>
|
||
<footer class="stack-version-footer" data-testid="stack-version-footer">
|
||
<div class="stack-version-footer__items">
|
||
<div class="stack-version-footer__item" v-for="item in footerItems" :key="item.key">
|
||
<span class="stack-version-footer__label">{{ item.label }}</span>
|
||
<span class="stack-version-footer__value" :class="valueClass(item.key)" :data-testid="`stack-version-${item.key}`">
|
||
{{ item.value }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<p v-if="footerMessage" class="stack-version-footer__warning" data-testid="stack-version-warning">
|
||
{{ footerMessage }}
|
||
</p>
|
||
</footer>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, ref } from 'vue';
|
||
import { DEFAULT_CHAIN_ID } from '@/config';
|
||
import { resolveGraphqlEndpoint } from '@/utils/graphqlRetry';
|
||
import { useVersionCheckOnMount } from '@/composables/useVersionCheck';
|
||
import logger from '@/utils/logger';
|
||
|
||
const endpointHint = ref<string | null>(null);
|
||
let endpoint: string | undefined;
|
||
try {
|
||
endpoint = resolveGraphqlEndpoint(DEFAULT_CHAIN_ID);
|
||
logger.info('Stack version footer resolved GraphQL endpoint', endpoint);
|
||
} catch (error) {
|
||
endpoint = undefined;
|
||
endpointHint.value = 'GraphQL endpoint unavailable – ensure the stack is running.';
|
||
logger.error('Failed to resolve GraphQL endpoint for stack version footer', error);
|
||
}
|
||
|
||
const { versionStatus, stackVersions, isChecking, hasChecked } = useVersionCheckOnMount(endpoint);
|
||
|
||
const footerItems = computed(() => {
|
||
const loading = isChecking.value && !hasChecked.value;
|
||
const loadingText = loading ? 'Loading…' : '—';
|
||
|
||
return [
|
||
{
|
||
key: 'contracts',
|
||
label: 'Contracts',
|
||
value: stackVersions.value.contractVersion !== null ? `v${stackVersions.value.contractVersion}` : loadingText,
|
||
},
|
||
{
|
||
key: 'ponder',
|
||
label: 'Ponder',
|
||
value: stackVersions.value.ponderVersion ? `v${stackVersions.value.ponderVersion}` : loadingText,
|
||
},
|
||
{
|
||
key: 'kraiken-lib',
|
||
label: 'kraiken-lib',
|
||
value: `v${stackVersions.value.kraikenLibVersion}`,
|
||
},
|
||
{
|
||
key: 'web-app',
|
||
label: 'Web App',
|
||
value: `v${stackVersions.value.webAppVersion}`,
|
||
},
|
||
] as const;
|
||
});
|
||
|
||
const footerMessage = computed(() => {
|
||
if (!versionStatus.value.isValid) {
|
||
return versionStatus.value.error ?? 'Stack version mismatch detected.';
|
||
}
|
||
return endpointHint.value;
|
||
});
|
||
|
||
function valueClass(key: (typeof footerItems.value)[number]['key']) {
|
||
if (key === 'kraiken-lib') {
|
||
return versionStatus.value.isValid ? null : 'stack-version-footer__value--warning';
|
||
}
|
||
return null;
|
||
}
|
||
</script>
|
||
|
||
<style lang="sass" scoped>
|
||
.stack-version-footer
|
||
padding: 12px 24px
|
||
background: rgba(7, 17, 27, 0.92)
|
||
color: #cbd5f5
|
||
border-top: 1px solid rgba(255, 255, 255, 0.08)
|
||
font-size: 12px
|
||
letter-spacing: 0.04em
|
||
display: flex
|
||
flex-direction: column
|
||
gap: 8px
|
||
align-items: center
|
||
width: 100%
|
||
|
||
.stack-version-footer__items
|
||
display: flex
|
||
flex-wrap: wrap
|
||
justify-content: center
|
||
gap: 16px
|
||
|
||
.stack-version-footer__item
|
||
display: flex
|
||
gap: 6px
|
||
align-items: baseline
|
||
|
||
.stack-version-footer__label
|
||
text-transform: uppercase
|
||
font-weight: 600
|
||
font-size: 11px
|
||
color: #94a3b8
|
||
|
||
.stack-version-footer__value
|
||
font-family: 'Fira Code', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace
|
||
font-weight: 600
|
||
color: #e2e8f0
|
||
|
||
.stack-version-footer__value--warning
|
||
color: #f97316
|
||
|
||
.stack-version-footer__warning
|
||
margin: 0
|
||
color: #f97316
|
||
font-size: 11px
|
||
text-align: center
|
||
</style>
|