Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(web): integrate RxDB for settings management and refactor settings handling #1815

Merged
merged 5 commits into from
Nov 20, 2024
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
3 changes: 3 additions & 0 deletions apps/desktop/src/renderer/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ declare module 'vue' {
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElMain: typeof import('element-plus/es')['ElMain']
Help: typeof import('./../../../../packages/ui/src/components/Help.vue')['default']
HelpView: typeof import('./../../../../packages/ui/src/components/HelpView.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SettingsView: typeof import('./../../../../packages/ui/src/components/SettingsView.vue')['default']
}
}
9 changes: 5 additions & 4 deletions apps/desktop/src/renderer/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
<script setup lang="ts">
import type { Lang } from 'mqttx'
import { ElementI18nMap } from '@mqttx/ui/i18n'
import { useSettingsStore } from '@mqttx/ui/stores'

const settingsStore = useSettingsStore()
console.log('MQTTX Desktop App init...')
const { locale } = useI18n()

console.log('MQTTX Web App init...')
</script>

<template>
<ElConfigProvider :locale="ElementI18nMap[settingsStore.lang]">
<ElConfigProvider :locale="ElementI18nMap[locale as Lang]">
<ElContainer>
<CommonLeftMenu />
<CommonMainView />
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/src/renderer/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { i18n } from '@mqttx/ui/i18n'
import { useSettingsStore } from '@mqttx/ui/stores'
// import { useSettingsStore } from '@mqttx/ui/stores'

import App from './App.vue'
import { router } from './router'
Expand All @@ -15,7 +15,7 @@ const pinia = createPinia()
app.use(router).use(pinia)

// I18n
const settingsStore = useSettingsStore()
i18n.global.locale = settingsStore.lang
// const { settings } = useSettingsStore()
// i18n.global.locale = settings.currentLang

app.use(i18n).mount('#app')
3 changes: 3 additions & 0 deletions apps/desktop/src/renderer/src/pages/help.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<HelpView />
</template>
4 changes: 4 additions & 0 deletions apps/desktop/src/renderer/src/pages/settings.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<template>
<!-- <SettingsView /> -->
TODO: Implement desktop database settings
</template>
2 changes: 2 additions & 0 deletions apps/desktop/typed-router.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,7 @@ declare module 'vue-router/auto-routes' {
export interface RouteNamedMap {
'/connections/': RouteRecordInfo<'/connections/', '/connections', Record<never, never>, Record<never, never>>,
'/connections/[id]': RouteRecordInfo<'/connections/[id]', '/connections/:id', { id: ParamValue<true> }, { id: ParamValue<false> }>,
'/help': RouteRecordInfo<'/help', '/help', Record<never, never>, Record<never, never>>,
'/settings': RouteRecordInfo<'/settings', '/settings', Record<never, never>, Record<never, never>>,
}
}
3 changes: 3 additions & 0 deletions apps/web/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ declare module 'vue' {
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElMain: typeof import('element-plus/es')['ElMain']
Help: typeof import('./../../packages/ui/src/components/Help.vue')['default']
HelpView: typeof import('./../../packages/ui/src/components/HelpView.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SettingsView: typeof import('./../../packages/ui/src/components/SettingsView.vue')['default']
}
}
2 changes: 2 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"@mqttx/ui": "workspace:*",
"element-plus": "^2.8.7",
"pinia": "^2.2.6",
"rxdb": "^15.38.3",
"rxjs": "^7.8.1",
"vue": "^3.5.12",
"vue-i18n": "^10.0.4",
"vue-router": "^4.4.5"
Expand Down
7 changes: 4 additions & 3 deletions apps/web/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
<script setup lang="ts">
import type { Lang } from 'mqttx'
import { ElementI18nMap } from '@mqttx/ui/i18n'
import { useSettingsStore } from '@mqttx/ui/stores'

const settingsStore = useSettingsStore()
const { locale } = useI18n()

console.log('MQTTX Web App init...')
</script>

<template>
<ElConfigProvider :locale="ElementI18nMap[settingsStore.lang]">
<ElConfigProvider :locale="ElementI18nMap[locale as Lang]">
<ElContainer>
<CommonLeftMenu />
<CommonMainView />
Expand Down
59 changes: 59 additions & 0 deletions apps/web/src/database/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { Plugin } from 'vue'
import { addRxPlugin, createRxDatabase } from 'rxdb'
import { getRxStorageDexie } from 'rxdb/plugins/storage-dexie'

// import typings
import type { RxMqttxCollections, RxMqttxDatabase } from './schemas/RxDB'

import settingsSchema from './schemas/Settings.schema'

// import modules
import { disableWarnings, RxDBDevModePlugin } from 'rxdb/plugins/dev-mode'
import { RxDBLeaderElectionPlugin } from 'rxdb/plugins/leader-election'
import { RxDBUpdatePlugin } from 'rxdb/plugins/update'
import { wrappedValidateAjvStorage } from 'rxdb/plugins/validate-ajv'

const KEY_DATABASE = Symbol('database')

if (import.meta.env.DEV) {
disableWarnings()
// in dev-mode we add the dev-mode plugin
// which does many checks and adds full error messages
addRxPlugin(RxDBDevModePlugin)
}
addRxPlugin(RxDBLeaderElectionPlugin)
addRxPlugin(RxDBUpdatePlugin)

export function useDatabase(): RxMqttxDatabase {
return inject<RxMqttxDatabase>(KEY_DATABASE) ?? window.db
}

export async function createDatabase(): Promise<Plugin> {
const db = await createRxDatabase<RxMqttxCollections>({
name: 'mqttx',
storage: wrappedValidateAjvStorage({
storage: getRxStorageDexie(),
}),
eventReduce: true,
})

// write to window for debugging
;(window as any).db = db

// show leadership in title
db.waitForLeadership().then(() => {
document.title = `♛ ${document.title}`
})

await db.addCollections({
settings: {
schema: settingsSchema,
},
})

return {
install(app: any) {
app.provide(KEY_DATABASE, db)
},
}
}
14 changes: 14 additions & 0 deletions apps/web/src/database/schemas/RxDB.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { RxDatabase } from 'rxdb'
import type { RxSettingsCollection } from './Settings.schema'

export interface RxMqttxCollections {
settings: RxSettingsCollection
}

export type RxMqttxDatabase = RxDatabase<RxMqttxCollections>

declare global {
interface Window {
db: RxMqttxDatabase
}
}
87 changes: 87 additions & 0 deletions apps/web/src/database/schemas/Settings.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type { Settings } from 'mqttx'
import type { RxCollection, RxDocument, RxJsonSchema } from 'rxdb'

export type RxSettingsDocumentType = Settings & { id: string }

// ORM methods
interface RxSettingsDocMethods {
// hpPercent: () => number
}

export type RxSettingsDocument = RxDocument<RxSettingsDocumentType, RxSettingsDocMethods>

export type RxSettingsCollection = RxCollection<RxSettingsDocumentType, RxSettingsDocMethods>

const settingsSchema: RxJsonSchema<RxSettingsDocumentType> = {
title: 'settings schema',
description: 'describes the settings',
version: 0,
keyCompression: false,
primaryKey: 'id',
type: 'object',
properties: {
id: {
type: 'string',
maxLength: 100, // <- the primary key must have set maxLength
},
currentLang: {
type: 'string',
default: 'en',
},
autoCheck: {
type: 'boolean',
default: true,
},
autoResub: {
type: 'boolean',
default: true,
},
multiTopics: {
type: 'boolean',
default: true,
},
maxReconnectTimes: {
type: 'number',
default: 10,
},
syncOsTheme: {
type: 'boolean',
default: false,
},
currentTheme: {
type: 'string',
default: 'light',
},
jsonHighlight: {
type: 'boolean',
default: true,
},
logLevel: {
type: 'string',
default: 'info',
},
ignoreQoS0Message: {
type: 'boolean',
default: false,
},
enableCopilot: {
type: 'boolean',
default: true,
},
openAIAPIHost: {
type: 'string',
default: 'https://api.openai.com/v1',
},
openAIAPIKey: {
type: 'string',
default: '',
},
model: {
type: 'string',
default: 'gpt-4o',
},
},
required: ['id'],
}

export default settingsSchema
36 changes: 36 additions & 0 deletions apps/web/src/database/services/SettingsService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { RxSettingsDocument, RxSettingsDocumentType } from '@/database/schemas/Settings.schema'

import type { Subscription } from 'rxjs'
import { useDatabase } from '@/database'
import { useSettingsStore } from '@mqttx/ui'

export default function useSettingsService() {
const db = useDatabase()
const { settings, updateSettings } = useSettingsStore()

async function getSettingsInDB(): Promise<Subscription> {
const data = await db.settings.findOne().exec() ?? await updateSettingsInDB()
const sub = data.$.subscribe((data) => {
const { ...settings } = data.toJSON()
updateSettings(settings)
})
return sub
}
async function updateSettingsInDB(data?: Partial<RxSettingsDocumentType>): Promise<RxSettingsDocument> {
const id = Math.random().toString(36).substring(2)
return db.settings.upsert(data ?? { id })
}

if (settings) {
watch(settings, (newSettings) => {
updateSettingsInDB(newSettings)
})
}

return {
settings: settings!,
updateSettings,
getSettingsInDB,
updateSettingsInDB,
}
}
21 changes: 14 additions & 7 deletions apps/web/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
import { i18n } from '@mqttx/ui/i18n'
import { useSettingsStore } from '@mqttx/ui/stores'

import App from './App.vue'
import { router } from './router'

import { createDatabase } from './database'
import useSettingsService from './database/services/SettingsService'

import { router } from './router'
import '@mqttx/ui/styles.scss'
import './assets/scss/main.scss'

const database = createDatabase()

// Create Vue
const app = createApp(App)

const pinia = createPinia()

app.use(router).use(pinia)

// I18n
const settingsStore = useSettingsStore()
i18n.global.locale = settingsStore.lang
database.then(async (db) => {
const { getSettingsInDB } = useSettingsService()
const sub = await getSettingsInDB()
const { settings } = useSettingsService()
i18n.global.locale = settings.currentLang
sub.unsubscribe()

app.use(i18n).mount('#app')
app.use(i18n).use(db).mount('#app')
})
3 changes: 3 additions & 0 deletions apps/web/src/pages/help.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<HelpView />
</template>
9 changes: 9 additions & 0 deletions apps/web/src/pages/settings.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script setup lang="ts">
import useSettingsService from '@/database/services/SettingsService'

const { settings } = useSettingsService()
</script>

<template>
<SettingsView v-model="settings" />
</template>
2 changes: 2 additions & 0 deletions apps/web/typed-router.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,7 @@ declare module 'vue-router/auto-routes' {
export interface RouteNamedMap {
'/connections/': RouteRecordInfo<'/connections/', '/connections', Record<never, never>, Record<never, never>>,
'/connections/[id]': RouteRecordInfo<'/connections/[id]', '/connections/:id', { id: ParamValue<true> }, { id: ParamValue<false> }>,
'/help': RouteRecordInfo<'/help', '/help', Record<never, never>, Record<never, never>>,
'/settings': RouteRecordInfo<'/settings', '/settings', Record<never, never>, Record<never, never>>,
}
}
21 changes: 21 additions & 0 deletions packages/types/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,27 @@ export type Lang = 'en' | 'zh' | 'ja' | 'hu' | 'tr'

export type Theme = 'light' | 'dark' | 'night'

export interface Settings {
// General
currentLang: Lang
autoCheck: boolean
autoResub: boolean
multiTopics: boolean
maxReconnectTimes: number
// Appearance
syncOsTheme: boolean
currentTheme: Theme
jsonHighlight: boolean
// Advanced
logLevel: 'debug' | 'info' | 'warn' | 'error'
ignoreQoS0Message: boolean
// MQTTX Copilot
enableCopilot: boolean
openAIAPIHost: string
openAIAPIKey: string
model: string
}

export default {}

export interface Connection {
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ declare module 'vue' {
ConnectionsListView: typeof import('./src/components/connections/ListView.vue')['default']
ElAside: typeof import('element-plus/es')['ElAside']
ElMain: typeof import('element-plus/es')['ElMain']
HelpView: typeof import('./src/components/HelpView.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SettingsView: typeof import('./src/components/SettingsView.vue')['default']
}
}
Loading
Loading