Skip to content

Commit d4edb9a

Browse files
author
hywax
committed
chore!: redesigned loading of dynamic services
1 parent e38a7a5 commit d4edb9a

File tree

9 files changed

+119
-61
lines changed

9 files changed

+119
-61
lines changed

components/Group.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
</template>
1313

1414
<script setup lang="ts">
15-
import type { BaseService } from '~/types'
15+
import type { Service } from '~/types'
1616
1717
export interface Props {
1818
title?: string
19-
items: BaseService[]
19+
items: Service[]
2020
}
2121
2222
defineProps<Props>()

components/Item.vue

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,35 @@
33
<Component :is="component" v-bind="props" />
44

55
<template #fallback>
6-
<ServicePlaceholder />
6+
<ServicePlaceholder :animate="false" />
77
</template>
88
</ClientOnly>
99
</template>
1010

1111
<script setup lang="ts">
12-
import { capitalize } from 'vue'
13-
import type { BaseService } from '~/types'
12+
import ServicePlaceholder from './service/Placeholder.vue'
13+
import type { Service } from '~/types'
1414
15-
const props = defineProps<BaseService>()
15+
const props = defineProps<Service>()
16+
17+
// At the moment there is a "flash" problem with the component.
18+
// It is necessary to fix it and remove manual mapping
19+
function resolveByTypeComponent(type: string) {
20+
// const name = capitalize(camelize(type))
21+
// return defineAsyncComponent({
22+
// loader: () => import(`~/components/service/${name}.vue`),
23+
// loadingComponent: ServicePlaceholder,
24+
// suspensible: false,
25+
// })
26+
27+
if (type === 'ip-api') {
28+
return resolveComponent('ServiceIpApi')
29+
}
30+
31+
return resolveComponent('ServiceBase')
32+
}
1633
1734
const component = props.type
18-
? defineAsyncComponent(() => import(`~/components/service/${capitalize(props.type!)}.vue`))
35+
? resolveByTypeComponent(props.type)
1936
: resolveComponent('ServiceBase')
2037
</script>

components/service/base/Index.vue

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11
<template>
2-
<div class="p-4 flex gap-4">
2+
<ServicePlaceholder v-if="loadingOverlay" />
3+
<div v-else class="p-4 flex gap-4">
34
<div class="flex-shrink-0 flex">
45
<a :href="link" :target="target" class="self-center w-16 h-16 overflow-hidden rounded-2xl border border-fg/10 dark:border-fg/15">
5-
<slot name="icon">
6+
<slot name="icon" :service="data">
67
<ServiceBaseIcon v-if="icon" v-bind="icon" />
78
</slot>
89
</a>
910
</div>
1011
<div>
1112
<h3 class="text-lg mt-1 font-semibold line-clamp-1 flex gap-2 items-center">
12-
<slot name="title">
13+
<slot name="title" :service="data">
1314
<a :href="link" :target="target">{{ title }}</a>
1415
</slot>
15-
<slot v-if="props?.status" name="status">
16+
<slot v-if="status" name="status" :data="data">
1617
<ServiceBaseStatus :ping="data?.ping" />
1718
</slot>
1819
</h3>
1920

2021
<p class="text-sm text-fg-dimmed line-clamp-1">
21-
<slot name="description">
22+
<slot name="description" :service="data">
2223
{{ description }}
2324
</slot>
2425
</p>
@@ -27,19 +28,27 @@
2728
</template>
2829

2930
<script setup lang="ts">
30-
import type { BaseService } from '~/types'
31-
import { useServiceData } from '~/composables/services'
31+
import type { Service, ServiceClient } from '~/types'
3232
33-
const props = defineProps<BaseService>()
33+
const props = defineProps<ServiceClient<Service>>()
3434
3535
const { $settings } = useNuxtApp()
3636
const target = computed(() => props.target || $settings.behaviour.target)
3737
38-
// Right now, queries for "base" are only needed for statuses.
39-
// When the situation will change it is necessary to remove/add condition for "immediate"
40-
const { data, pauseUpdate } = useServiceData<BaseService, { ping: { time: number, status: boolean } }>(props, {
41-
immediate: props.status?.enabled || false,
38+
const immediate = computed(() => props.status?.enabled || !!props.type || false)
39+
const { data, pauseUpdate } = useServiceData<Service>(props, {
40+
immediate: immediate.value,
4241
})
4342
43+
const loadingOverlay = computed(() => {
44+
if (props.type && !data.value) {
45+
return true
46+
}
47+
48+
return false
49+
})
50+
51+
defineExpose({ data })
52+
4453
onBeforeUnmount(pauseUpdate)
4554
</script>

composables/services.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ export interface ServiceDataOptions {
55
updateInterval?: number
66
}
77

8-
export function useServiceData<T extends BaseService, R>(service: T, options?: ServiceDataOptions) {
8+
export function useServiceData<T extends BaseService>(service: T, options?: ServiceDataOptions): any {
99
const immediate = options?.immediate || false
1010
const updateInterval = (options?.updateInterval || 60) * 1000
1111
const type = service.type || 'base'
1212

13-
const { data, pending, status, refresh, execute } = useFetch<R>(`/api/services/${type}`, {
13+
const { data, pending, status, refresh, execute } = useFetch(`/api/services/${type}`, {
1414
immediate,
1515
query: { id: service.id },
1616
timeout: 15000,

server/api/services/base.ts

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,8 @@
11
import type { BaseService } from '~/types'
2+
import { getServiceWithDefaultData, returnServiceWithData } from '~/server/utils/services'
23

3-
export interface State {
4-
ping: {
5-
status: boolean
6-
time: number
7-
}
8-
}
4+
export default defineEventHandler(async (event) => {
5+
const service = await getServiceWithDefaultData<BaseService>(event)
96

10-
export default defineEventHandler(async (event): Promise<State> => {
11-
const service = await getService<BaseService>(event)
12-
const state: State = {
13-
ping: {
14-
status: false,
15-
time: -1,
16-
},
17-
}
18-
19-
if (service?.status?.enabled) {
20-
try {
21-
const startTime = new Date().getTime()
22-
await $fetch(service.link, { timeout: 15000 })
23-
const endTime = new Date().getTime()
24-
25-
state.ping = {
26-
status: true,
27-
time: endTime - startTime,
28-
}
29-
} catch (e) {
30-
logger.error(e)
31-
}
32-
}
33-
34-
return state
7+
return returnServiceWithData(service, {})
358
})

server/utils/config.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import crypto from 'node:crypto'
22
import yaml from 'yaml'
33
import defu from 'defu'
44
import { ZodError, z } from 'zod'
5-
import type { BaseService, CompleteConfig } from '~/types'
5+
import type { CompleteConfig, Service } from '~/types'
66

7-
type DraftService = Omit<BaseService, 'id'>
7+
type DraftService = Omit<Service, 'id'>
88

9-
function determineServiceId(items: DraftService[]): BaseService[] {
9+
function determineServiceId(items: DraftService[]): Service[] {
1010
return items.map((item) => ({
1111
id: crypto.randomUUID(),
1212
...item,
@@ -137,8 +137,8 @@ export function extractSafelyConfig(config: CompleteConfig) {
137137
/**
138138
* Create Map services
139139
*/
140-
export function extractServicesFromConfig(config: CompleteConfig): Record<string, BaseService> {
141-
return config.services.reduce<Record<string, BaseService>>((acc, group) => {
140+
export function extractServicesFromConfig(config: CompleteConfig): Record<string, Service> {
141+
return config.services.reduce<Record<string, Service>>((acc, group) => {
142142
for (const item of group.items) {
143143
acc[item.id] = item
144144
}

server/utils/services.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,27 @@
11
import type { H3Event } from 'h3'
2+
import type { PingServiceData, ReturnServiceWithData, Service, ServiceWithDefaultData } from '~/types'
23

3-
export async function getService<T>(event: H3Event): Promise<T> {
4+
export async function pingService(url: string): Promise<PingServiceData> {
5+
try {
6+
const startTime = new Date().getTime()
7+
await $fetch(url, { timeout: 15000 })
8+
const endTime = new Date().getTime()
9+
10+
return {
11+
status: true,
12+
time: endTime - startTime,
13+
}
14+
} catch (e) {
15+
logger.error(e)
16+
}
17+
18+
return {
19+
status: false,
20+
time: 0,
21+
}
22+
}
23+
24+
export async function getService<T extends Service>(event: H3Event): Promise<T> {
425
const { id } = getQuery<{ id?: string }>(event)
526

627
if (!id) {
@@ -22,3 +43,22 @@ export async function getService<T>(event: H3Event): Promise<T> {
2243

2344
return services[id]
2445
}
46+
47+
export async function getServiceWithDefaultData<S extends Service>(event: H3Event): Promise<ServiceWithDefaultData<S>> {
48+
const config = await getService<S>(event)
49+
const defaultData = {
50+
ping: config?.status?.enabled ? await pingService(config.link) : undefined,
51+
}
52+
53+
return { config, defaultData }
54+
}
55+
56+
export function returnServiceWithData<
57+
S extends ServiceWithDefaultData<Service>,
58+
D extends S['config']['server'] = S['config']['server'],
59+
>(service: S, data: D): ReturnServiceWithData<D, S['defaultData']> {
60+
return {
61+
...service.defaultData,
62+
data,
63+
}
64+
}

types/config.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import type { BaseService } from '~/types/services'
1+
import type { Service } from '~/types/services'
22

33
export interface ServicesGroup {
44
title?: string
5-
items: BaseService[]
5+
items: Service[]
66
}
77

88
export interface Behaviour {

types/services.d.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export interface ServiceIcon {
1111
color?: string
1212
}
1313

14-
export interface BaseService {
14+
export interface Service {
1515
id: string
1616
type?: string
1717
title: string
@@ -22,4 +22,23 @@ export interface BaseService {
2222
status?: ServiceStatus
2323
options?: Record<string, string | number | boolean>
2424
secrets?: Record<string, string | number | boolean>
25+
server?: Record<string, string | number | boolean>
2526
}
27+
28+
export type ServiceClient<T> = Omit<T, 'secrets' | 'server'>
29+
30+
export interface PingServiceData {
31+
status: boolean
32+
time: number
33+
}
34+
35+
export interface ServiceWithDefaultData<S> {
36+
config: S
37+
defaultData: {
38+
ping?: PingServiceData
39+
}
40+
}
41+
42+
export type ReturnServiceWithData<D, S extends ServiceWithDefaultData<Service>['defaultData']> = S & { data: D }
43+
44+
export interface BaseService extends Service {}

0 commit comments

Comments
 (0)