feat(ui): Credentials + OpenWrtGateway render from cached resources — B4 complete
Some checks failed
Demo images / Build & push demo images (push) Failing after 2m10s
Some checks failed
Demo images / Build & push demo images (push) Failing after 2m10s
- Credentials: identity.list + identity.list-credentials become useCachedResource entries; explicit reloads still toast on failure. - OpenWrtGateway: openwrt.get-status caches in the shared store (revisits paint the last router state instantly); the connect flow's params/No-router-configured semantics are preserved on top of the entry. - ContainerApps assessed and left as-is: its Pinia store already persists across navigation, keeps last data on error, and gates the spinner on empty — same class as Apps/Marketplace/Fleet. This closes the B4 rollout list from docs/FIPS-UPTIME-AND-UI-STATE-PLAN.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
8fd72b947a
commit
8907cc47d9
@ -199,8 +199,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { useCachedResource } from '@/composables/useCachedResource'
|
||||
import BackButton from '@/components/BackButton.vue'
|
||||
|
||||
interface Identity {
|
||||
@ -228,9 +229,30 @@ interface Credential {
|
||||
status: string
|
||||
}
|
||||
|
||||
const identities = ref<Identity[]>([])
|
||||
const credentials = ref<Credential[]>([])
|
||||
const loadingCreds = ref(false)
|
||||
// Cached: revisits paint identities/credentials instantly and revalidate
|
||||
// behind them (errors keep the last-known lists).
|
||||
const identitiesRes = useCachedResource<Identity[]>({
|
||||
key: 'credentials.identities',
|
||||
fetcher: async (signal) => {
|
||||
const result = await rpcClient.call<{ identities: Identity[] }>({
|
||||
method: 'identity.list', params: {}, signal, dedup: true, maxRetries: 1,
|
||||
})
|
||||
return result?.identities || []
|
||||
},
|
||||
})
|
||||
const credentialsRes = useCachedResource<Credential[]>({
|
||||
key: 'credentials.list',
|
||||
fetcher: async (signal) => {
|
||||
const result = await rpcClient.call<{ credentials: Credential[] }>({
|
||||
method: 'identity.list-credentials', params: {}, signal, dedup: true, maxRetries: 1,
|
||||
})
|
||||
return result?.credentials || []
|
||||
},
|
||||
})
|
||||
const identities = computed(() => identitiesRes.data.value ?? [])
|
||||
const credentials = computed(() => credentialsRes.data.value ?? [])
|
||||
const loadingCreds = computed(() =>
|
||||
credentialsRes.loadState.value === 'loading' || credentialsRes.loadState.value === 'refreshing')
|
||||
const selectedCredential = ref<Credential | null>(null)
|
||||
const credCopied = ref(false)
|
||||
const revoking = ref(false)
|
||||
@ -280,31 +302,10 @@ function formatClaims(subject: Record<string, unknown>): string {
|
||||
return JSON.stringify(claims, null, 2)
|
||||
}
|
||||
|
||||
async function loadIdentities() {
|
||||
try {
|
||||
const result = await rpcClient.call<{ identities: Identity[] }>({
|
||||
method: 'identity.list',
|
||||
params: {},
|
||||
})
|
||||
identities.value = result.identities || []
|
||||
} catch (e) {
|
||||
identities.value = []
|
||||
if (import.meta.env.DEV) console.warn('Failed to load identities:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCredentials() {
|
||||
loadingCreds.value = true
|
||||
try {
|
||||
const result = await rpcClient.call<{ credentials: Credential[] }>({
|
||||
method: 'identity.list-credentials',
|
||||
params: {},
|
||||
})
|
||||
credentials.value = result.credentials || []
|
||||
} catch (e) {
|
||||
showToast(`Failed to load credentials: ${e instanceof Error ? e.message : 'Unknown error'}`, 'error')
|
||||
} finally {
|
||||
loadingCreds.value = false
|
||||
await credentialsRes.refresh()
|
||||
if (credentialsRes.error.value) {
|
||||
showToast(`Failed to load credentials: ${credentialsRes.error.value}`, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
@ -404,9 +405,8 @@ async function copyCredentialJson() {
|
||||
setTimeout(() => { credCopied.value = false }, 2000)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadIdentities(), loadCredentials()])
|
||||
})
|
||||
// Both resources fetch themselves on first use (skipping the fetch entirely
|
||||
// when the cached value is fresh).
|
||||
|
||||
defineExpose({ credentials, loadCredentials })
|
||||
</script>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createPinia } from 'pinia'
|
||||
import Credentials from '../Credentials.vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
|
||||
@ -42,6 +43,8 @@ describe('Credentials', () => {
|
||||
|
||||
const wrapper = mount(Credentials, {
|
||||
global: {
|
||||
// The cached-resource layer pulls the Pinia resources store in setup.
|
||||
plugins: [createPinia()],
|
||||
mocks: {
|
||||
$router: { push: vi.fn() },
|
||||
},
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { useResourcesStore } from '@/stores/resources'
|
||||
import BackButton from '@/components/BackButton.vue'
|
||||
|
||||
const router = useRouter()
|
||||
@ -73,8 +74,15 @@ interface ScannedNetwork {
|
||||
encryption: string
|
||||
}
|
||||
|
||||
const status = ref<RouterStatus | null>(null)
|
||||
const loading = ref(true)
|
||||
const status = computed(() => statusEntry().data)
|
||||
// Router status is cached in the shared resources store so revisits paint
|
||||
// the last-known state instantly while a fresh read runs behind it.
|
||||
const resources = useResourcesStore()
|
||||
const statusEntry = () => resources.entry<RouterStatus>('server.openwrt-status')
|
||||
const loading = computed(() => {
|
||||
const s = statusEntry().loadState
|
||||
return s === 'loading' || s === 'refreshing' || (s === 'idle' && statusEntry().data === null)
|
||||
})
|
||||
const error = ref('')
|
||||
const host = ref('')
|
||||
const sshUser = ref('root')
|
||||
@ -115,25 +123,25 @@ const dhcpLimit = ref(150)
|
||||
const masqEnabled = ref(true)
|
||||
|
||||
async function load(params?: Record<string, string>) {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
status.value = await rpcClient.call<RouterStatus>({
|
||||
await resources.refresh<RouterStatus>('server.openwrt-status', () =>
|
||||
rpcClient.call<RouterStatus>({
|
||||
method: 'openwrt.get-status',
|
||||
params: params ?? {},
|
||||
timeout: 30000,
|
||||
})
|
||||
showConnectForm.value = false
|
||||
if (params) connectedParams.value = params
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
if (msg.includes('No router configured')) {
|
||||
dedup: true,
|
||||
maxRetries: 1,
|
||||
}))
|
||||
const err = statusEntry().error
|
||||
if (err) {
|
||||
if (err.includes('No router configured')) {
|
||||
showConnectForm.value = true
|
||||
} else {
|
||||
error.value = msg
|
||||
error.value = err
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
} else {
|
||||
showConnectForm.value = false
|
||||
if (params) connectedParams.value = params
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user