opsdash-app/opsdash/composables/useConfigExportImport.ts
2025-12-26 09:13:18 +07:00

156 lines
5.6 KiB
TypeScript

import type { Ref } from 'vue'
import { cloneTargetsConfig, normalizeTargetsConfig, type TargetsConfig } from '../src/services/targets'
import { buildConfigEnvelope, sanitiseSidebarPayload } from '../src/utils/sidebarConfig'
import type { OnboardingState } from './useDashboard'
import type { ThemePreference } from './useThemeController'
import type { WidgetTabsState } from '../src/services/widgetsRegistry'
import { createDefaultWidgetTabs, normalizeWidgetTabs } from '../src/services/widgetsRegistry'
interface ConfigExportImportDeps {
selected: Ref<string[]>
groupsById: Ref<Record<string, number>>
targetsWeek: Ref<Record<string, number>>
targetsMonth: Ref<Record<string, number>>
targetsConfig: Ref<TargetsConfig>
themePreference: Ref<ThemePreference>
onboardingState: Ref<OnboardingState | null>
widgetTabs?: Ref<WidgetTabsState>
setThemePreference: (value: ThemePreference, options?: { persist?: boolean }) => void
postJson: (url: string, body: Record<string, unknown>) => Promise<any>
route: (name: 'persist') => string
performLoad: () => Promise<void> | void
notifySuccess: (message: string) => void
notifyError: (message: string) => void
createDownload?: (filename: string, data: unknown) => void
}
export function useConfigExportImport(deps: ConfigExportImportDeps) {
function collectSidebarPayload() {
const payload: Record<string, any> = {
cals: [...deps.selected.value],
groups: { ...deps.groupsById.value },
targets_week: { ...deps.targetsWeek.value },
targets_month: { ...deps.targetsMonth.value },
targets_config: cloneTargetsConfig(deps.targetsConfig.value),
theme_preference: deps.themePreference.value,
}
if (deps.onboardingState.value) {
payload.onboarding = { ...deps.onboardingState.value }
}
if (deps.widgetTabs) {
payload.widgets = deps.widgetTabs.value
}
return payload
}
function downloadJson(filename: string, data: unknown) {
if (deps.createDownload) {
deps.createDownload(filename, data)
return
}
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
function exportSidebarConfig() {
try {
const payload = collectSidebarPayload()
const envelope = buildConfigEnvelope(payload)
const filename = `opsdash-config-${envelope.generated.slice(0, 10)}.json`
downloadJson(filename, envelope)
deps.notifySuccess('Configuration exported')
} catch (error) {
console.error(error)
deps.notifyError('Failed to export configuration')
}
}
async function applyConfigSource(source: unknown) {
const { cleaned, ignored } = sanitiseSidebarPayload(source)
if (!Object.keys(cleaned).length) {
deps.notifyError('No recognised configuration keys found in file')
return
}
if (cleaned.cals) {
deps.selected.value = (cleaned.cals as any[]).map((id) => String(id))
}
if (cleaned.groups) {
const nextGroups: Record<string, number> = {}
Object.entries(cleaned.groups as Record<string, any>).forEach(([key, value]) => {
const num = Number(value)
nextGroups[String(key)] = Number.isFinite(num) ? num : 0
})
deps.groupsById.value = nextGroups
}
if (cleaned.targets_week) {
const nextWeek: Record<string, number> = {}
Object.entries(cleaned.targets_week as Record<string, any>).forEach(([key, value]) => {
const num = Number(value)
if (Number.isFinite(num)) {
nextWeek[String(key)] = num
}
})
deps.targetsWeek.value = nextWeek
}
if (cleaned.targets_month) {
const nextMonth: Record<string, number> = {}
Object.entries(cleaned.targets_month as Record<string, any>).forEach(([key, value]) => {
const num = Number(value)
if (Number.isFinite(num)) {
nextMonth[String(key)] = num
}
})
deps.targetsMonth.value = nextMonth
}
if (cleaned.targets_config) {
deps.targetsConfig.value = normalizeTargetsConfig(cleaned.targets_config as TargetsConfig)
}
if (cleaned.theme_preference) {
deps.setThemePreference(cleaned.theme_preference as ThemePreference, { persist: false })
}
if (cleaned.onboarding && typeof cleaned.onboarding === 'object') {
deps.onboardingState.value = { ...(cleaned.onboarding as OnboardingState) }
}
if (deps.widgetTabs && (cleaned as any).widgets) {
const fallback = deps.widgetTabs.value || createDefaultWidgetTabs('standard')
deps.widgetTabs.value = normalizeWidgetTabs((cleaned as any).widgets, fallback)
}
await deps.postJson(deps.route('persist'), cleaned as Record<string, unknown>)
await deps.performLoad()
if (ignored.length) {
deps.notifyError(`Configuration imported with ignored keys: ${ignored.join(', ')}`)
} else {
deps.notifySuccess('Configuration imported')
}
}
async function importSidebarConfig(file: File) {
try {
const text = await file.text()
const parsed = JSON.parse(text)
const source = parsed?.payload && typeof parsed.payload === 'object' ? parsed.payload : parsed
await applyConfigSource(source)
} catch (error) {
console.error(error)
deps.notifyError('Failed to import configuration')
}
}
return {
collectSidebarPayload,
exportSidebarConfig,
importSidebarConfig,
applyConfigSource,
}
}