From 5d950cf8c2565b384d9f0e90b25411b1d0c2b49b Mon Sep 17 00:00:00 2001 From: GermanBluefox Date: Sat, 5 Oct 2024 21:11:01 +0800 Subject: [PATCH] Implemented better display of Adapter Update rule: https://github.com/ioBroker/ioBroker.admin/issues/2731 --- package-lock.json | 12 +- package.json | 2 +- packages/admin/src-admin/src/App.tsx | 4 +- .../src-admin/src/Workers/AdaptersWorker.tsx | 159 +---------- .../src-admin/src/Workers/GenericWorker.tsx | 250 ++++++++++++++++++ .../src/Workers/HostAdapterWorker.tsx | 28 ++ .../src-admin/src/Workers/HostsWorker.tsx | 149 +---------- .../src-admin/src/Workers/InstancesWorker.tsx | 158 +---------- .../src-admin/src/Workers/ObjectsWorker.tsx | 6 - .../components/Adapters/AdapterGeneric.tsx | 17 +- .../Adapters/AdapterInstallDialog.tsx | 4 +- .../src/components/HostSelectors.tsx | 2 +- .../src/dialogs/AddInstanceDialog.tsx | 2 +- .../src/dialogs/AutoUpgradeConfigDialog.tsx | 28 +- .../admin/src-admin/src/tabs/Adapters.tsx | 46 +++- .../admin/src-admin/src/tabs/CustomTab.tsx | 4 +- packages/admin/src-admin/src/tabs/Hosts.tsx | 11 +- .../admin/src-admin/src/tabs/Instances.tsx | 2 +- packages/dm-gui-components/package.json | 2 +- packages/jsonConfig/package.json | 2 +- 20 files changed, 405 insertions(+), 483 deletions(-) create mode 100644 packages/admin/src-admin/src/Workers/GenericWorker.tsx create mode 100644 packages/admin/src-admin/src/Workers/HostAdapterWorker.tsx diff --git a/package-lock.json b/package-lock.json index f95d07084..4e0d63be4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,7 @@ "@fnando/sparkline": "^0.3.10", "@foxriver76/iob-component-lib": "^0.1.6", "@honkhonk/vite-plugin-svgr": "^1.1.0", - "@iobroker/adapter-react-v5": "^7.2.2", + "@iobroker/adapter-react-v5": "^7.2.3", "@iobroker/admin-component-easy-access": "^1.0.1", "@iobroker/build-tools": "^2.0.5", "@iobroker/dm-utils": "^0.5.0", @@ -3366,9 +3366,9 @@ } }, "node_modules/@iobroker/adapter-react-v5": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/@iobroker/adapter-react-v5/-/adapter-react-v5-7.2.2.tgz", - "integrity": "sha512-hxtGmRccqiM5EWc297Htp3rG7tf/He2lnZ/Gm5EBTrhx4yA2wDIu5WBnoleUDrGIsqO/qF9vJvuuDr6pV1GPxA==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/@iobroker/adapter-react-v5/-/adapter-react-v5-7.2.3.tgz", + "integrity": "sha512-Pa9zFEVjpoDH9B0CRfxvzDfPRzN0F94KqsI0SVDDhW7QqCjtroi35QPZR/PuSeUmpTd/P43R6k/KUQBdxk7QEA==", "license": "MIT", "dependencies": { "@emotion/react": "^11.13.3", @@ -45665,7 +45665,7 @@ "version": "7.2.0", "license": "MIT", "dependencies": { - "@iobroker/adapter-react-v5": "^7.2.2", + "@iobroker/adapter-react-v5": "^7.2.3", "@iobroker/json-config": "file:../jsonConfig" } }, @@ -45673,7 +45673,7 @@ "name": "@iobroker/json-config", "version": "7.2.0", "dependencies": { - "@iobroker/adapter-react-v5": "^7.2.2", + "@iobroker/adapter-react-v5": "^7.2.3", "crypto-js": "^4.2.0", "react-ace": "^12.0.0" } diff --git a/package.json b/package.json index 7c6a04d1f..3b00d4aea 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@fnando/sparkline": "^0.3.10", "@foxriver76/iob-component-lib": "^0.1.6", "@honkhonk/vite-plugin-svgr": "^1.1.0", - "@iobroker/adapter-react-v5": "^7.2.2", + "@iobroker/adapter-react-v5": "^7.2.3", "@iobroker/admin-component-easy-access": "^1.0.1", "@iobroker/build-tools": "^2.0.5", "@iobroker/dm-utils": "^0.5.0", diff --git a/packages/admin/src-admin/src/App.tsx b/packages/admin/src-admin/src/App.tsx index 03b536676..7867b3ce6 100644 --- a/packages/admin/src-admin/src/App.tsx +++ b/packages/admin/src-admin/src/App.tsx @@ -1564,7 +1564,7 @@ class App extends Router { } } - const instances = await this.instancesWorker.getInstances(); + const instances = await this.instancesWorker.getObjects(); this.setState({ noNotifications, notifications: { notifications, instances } }); }; @@ -1586,7 +1586,7 @@ class App extends Router { const result = notifications[host].result; if (result?.system && Object.keys(result.system.categories).length) { - await this.instancesWorker.getInstances().then(instances => + await this.instancesWorker.getObjects().then(instances => this.setState({ showHostWarning: { host, diff --git a/packages/admin/src-admin/src/Workers/AdaptersWorker.tsx b/packages/admin/src-admin/src/Workers/AdaptersWorker.tsx index 7d1d54e73..68deb3f03 100644 --- a/packages/admin/src-admin/src/Workers/AdaptersWorker.tsx +++ b/packages/admin/src-admin/src/Workers/AdaptersWorker.tsx @@ -1,166 +1,27 @@ import { type AdminConnection } from '@iobroker/adapter-react-v5'; -import AdminUtils from '../helpers/AdminUtils'; +import GenericWorker, { type EventType, type GenericEvent } from './GenericWorker'; -export type AdapterEventType = 'new' | 'changed' | 'deleted'; +export type AdapterEventType = EventType; -export interface AdapterEvent { - id: string; - obj?: ioBroker.AdapterObject; - type: AdapterEventType; - oldObj?: ioBroker.AdapterObject; -} - -export default class AdaptersWorker { - private readonly socket: AdminConnection; - - private readonly handlers: ((events: AdapterEvent[]) => void)[]; +export type AdapterEvent = GenericEvent<'adapter'>; +export default class AdaptersWorker extends GenericWorker<'adapter'> { private readonly repositoryHandlers: (() => void)[]; - private promise: Promise> | null; - - private forceUpdate: boolean; - - private connected: boolean; - - private objects: Record | null; - private repoTimer: ReturnType | null; constructor(socket: AdminConnection) { - this.socket = socket; - this.handlers = []; + super(socket, 'system.adapter', 'adapter'); this.repositoryHandlers = []; - this.promise = null; - this.forceUpdate = false; - - socket.registerConnectionHandler(this.connectionHandler); - this.connected = this.socket.isConnected(); - - this.objects = null; } - objectChangeHandler = (id: string, obj: ioBroker.AdapterObject): void => { - this.objects = this.objects || {}; - // if instance - if (id.match(/^system\.adapter\.[^.]+$/)) { - let type: AdapterEventType; - let oldObj: ioBroker.AdapterObject | undefined; - - if (obj) { - if (obj.type !== 'adapter') { - return; - } - - AdminUtils.fixAdminUI(obj); - - if (this.objects[id]) { - oldObj = this.objects[id]; - if (JSON.stringify(this.objects[id]) !== JSON.stringify(obj)) { - type = 'changed'; - this.objects[id] = obj; - } else { - // no changes - return; - } - } else { - type = 'new'; - this.objects[id] = obj; - } - } else if (this.objects[id]) { - oldObj = this.objects[id]; - type = 'deleted'; - delete this.objects[id]; - } else { - // deleted unknown instance - return; - } - - this.socket.getAdaptersResetCache(); - this.socket.getInstalledResetCache(''); - this.forceUpdate = true; - this.promise = null; - - this.handlers.forEach(cb => - cb([ - { - id, - obj, - type, - oldObj, - }, - ]), - ); - } - }; - - isForceUpdate(): boolean { - return this.forceUpdate; + protected checkObjectId(id: string, obj: ioBroker.AdapterObject | null | undefined): boolean { + return id.match(/^system\.adapter\.[^.]+$/) && (!obj || obj.type === this.objectType); } - // be careful with this object. Do not change them. - getAdapters(update?: boolean): Promise> { - if (!update && this.promise instanceof Promise) { - return this.promise; - } - - update = update || this.forceUpdate; - this.forceUpdate = false; - - this.promise = this.socket - .getAdapters(null, update) - .then(objects => { - this.objects = {}; - objects.forEach(obj => (this.objects[obj._id] = obj)); - return this.objects; - }) - .catch(e => window.alert(`Cannot get adapters: ${e}`)); - - return this.promise; - } - - connectionHandler = (isConnected: boolean): void => { - if (isConnected && !this.connected) { - this.connected = true; - - if (this.handlers.length) { - this.socket - .subscribeObject('system.adapter.*', this.objectChangeHandler) - .catch(e => window.alert(`Cannot subscribe on object: ${e}`)); - - void this.getAdapters(true).then( - adapters => - adapters && Object.keys(adapters).forEach(id => this.objectChangeHandler(id, adapters[id])), - ); - } - } else if (!isConnected && this.connected) { - this.connected = false; - } - }; - - registerHandler(cb: (events: AdapterEvent[]) => void): void { - if (!this.handlers.includes(cb)) { - this.handlers.push(cb); - - if (this.handlers.length === 1 && this.connected) { - this.socket - .subscribeObject('system.adapter.*', this.objectChangeHandler) - .catch(e => window.alert(`Cannot subscribe on object: ${e}`)); - } - } - } - - unregisterHandler(cb: (events: AdapterEvent[]) => void): void { - const pos = this.handlers.indexOf(cb); - if (pos !== -1) { - this.handlers.splice(pos, 1); - } - - if (!this.handlers.length && this.connected) { - this.socket - .unsubscribeObject('system.adapter.*', this.objectChangeHandler) - .catch(e => window.alert(`Cannot unsubscribe on object: ${e}`)); - } + protected postProcessing(_id: string, _obj: ioBroker.AdapterObject | null | undefined): void { + this.socket.getAdaptersResetCache(); + this.socket.getInstalledResetCache(''); } repoChangeHandler = (/* id, obj */): void => { diff --git a/packages/admin/src-admin/src/Workers/GenericWorker.tsx b/packages/admin/src-admin/src/Workers/GenericWorker.tsx new file mode 100644 index 000000000..e81e2454d --- /dev/null +++ b/packages/admin/src-admin/src/Workers/GenericWorker.tsx @@ -0,0 +1,250 @@ +import type { AdminConnection } from '@iobroker/adapter-react-v5'; +import AdminUtils from '@/helpers/AdminUtils'; + +export type EventType = 'new' | 'changed' | 'deleted'; + +export type GetObjectFromType = T extends 'host' + ? ioBroker.HostObject + : T extends 'adapter' + ? ioBroker.AdapterObject + : T extends 'instance' + ? ioBroker.InstanceObject + : T extends 'meta' + ? ioBroker.MetaObject + : T extends 'device' + ? ioBroker.DeviceObject + : T extends 'channel' + ? ioBroker.ChannelObject + : T extends 'state' + ? ioBroker.StateObject + : T extends 'folder' + ? ioBroker.FolderObject + : T extends 'enum' + ? ioBroker.EnumObject + : T extends 'script' + ? ioBroker.ScriptObject + : T extends 'group' + ? ioBroker.GroupObject + : T extends 'user' + ? ioBroker.UserObject + : T extends 'chart' + ? ioBroker.ChartObject + : T extends 'schedule' + ? ioBroker.ScheduleObject + : ioBroker.Object; + +type GetRootFromType = T extends 'host' + ? `system.host.${string}` + : T extends 'adapter' + ? `system.adapter.${string}` | `system.host.${string}.adapter.${string}` + : T extends 'instance' + ? `system.adapter.${string}.${number}` + : T extends 'enum' + ? `system.enum.${string}` + : T extends 'script' + ? `script.js.${string}` + : T extends 'group' + ? `system.group.${string}` + : T extends 'user' + ? `system.user.${string}` + : string; + +export type GenericEvent = { + id: GetRootFromType; + obj?: GetObjectFromType; + type: EventType; + oldObj?: GetObjectFromType; +}; + +export default class GenericWorker { + protected readonly socket: AdminConnection; + + protected readonly handlers: ((events: GenericEvent[]) => void)[] = []; + + private promise: Promise>> | null = null; + + protected connected: boolean; + + protected objects: Record> | null = null; + + private readonly root: string; + + protected readonly objectType: ioBroker.ObjectType; + + private forceUpdate: boolean = false; + + protected constructor(socket: AdminConnection, root: string, objectType: ioBroker.ObjectType) { + this.socket = socket; + this.root = root; + this.objectType = objectType; + + socket.registerConnectionHandler(this.connectionHandler); + + this.connected = this.socket.isConnected(); + if (this.connected) { + this.connectionHandler(true); + } + } + + protected checkObjectId(id: string, obj: GetObjectFromType | null | undefined): boolean { + return id.startsWith(`${this.root}.`) && (!obj || obj.type === this.objectType); + } + + // eslint-disable-next-line class-methods-use-this + protected postProcessing(_id: string, _obj: GetObjectFromType | null | undefined): void { + // can be overridden in the child class + } + + isForceUpdate(): boolean { + return this.forceUpdate; + } + + objectChangeHandler = (id: GetRootFromType, obj: GetObjectFromType | null | undefined): void => { + this.objects = this.objects || {}; + + // if our object + if (this.checkObjectId(id, obj)) { + let type: EventType; + let oldObj: GetObjectFromType | undefined; + if (obj) { + if (this.objectType === 'adapter' || this.objectType === 'instance') { + AdminUtils.fixAdminUI(obj); + } + if (this.objects[id]) { + if (JSON.stringify(this.objects[id]) !== JSON.stringify(obj)) { + type = 'changed'; + this.objects[id] = obj; + } else { + // no changes + return; + } + } else { + type = 'new'; + this.objects[id] = obj; + } + } else if (this.objects[id]) { + type = 'deleted'; + oldObj = this.objects[id]; + delete this.objects[id]; + } else { + // deleted unknown object + return; + } + + this.forceUpdate = true; + + this.postProcessing(id, obj); + + this.handlers.forEach(cb => + cb([ + { + id, + obj, + type, + oldObj, + }, + ]), + ); + } + }; + + getObjects(update?: boolean): Promise>> { + update = update || this.forceUpdate; + this.forceUpdate = false; + + if (!update && this.promise instanceof Promise) { + return this.promise; + } + + this.promise = this.socket + .getObjectViewSystem( + this.objectType, + this.root ? `${this.root}.` : '', + this.root ? `${this.root}.\u9999` : '\u9999', + ) + .then(objects => { + this.objects = objects as Record>; + if (this.objectType === 'adapter' || this.objectType === 'instance') { + Object.keys(this.objects).forEach(id => AdminUtils.fixAdminUI(this.objects[id])); + } + + return this.objects; + }) + .catch(e => { + window.alert(`Cannot get objects of type ${this.objectType}, with root "${this.root}": ${e}`); + return null; + }); + + return this.promise; + } + + // eslint-disable-next-line class-methods-use-this + protected connectionPostHandler(_isConnected: boolean): void {} + + connectionHandler = (isConnected: boolean): void => { + if (isConnected && !this.connected) { + this.connected = true; + + if (this.handlers.length) { + this.socket + .subscribeObject(this.root ? `${this.root}.*` : '*', this.objectChangeHandler) + .then(() => { + // read all hosts anew and inform about it + void this.getObjects(true).then(objects => { + if (objects) { + Object.keys(objects).forEach(id => + this.objectChangeHandler(id as GetRootFromType, objects[id]), + ); + } + }); + }) + .catch(e => window.alert(`Cannot subscribe on object "${this.root}": ${e}`)); + } + this.connectionPostHandler(true); + } else if (!isConnected && this.connected) { + this.connected = false; + this.connectionPostHandler(false); + } + }; + + registerHandler(cb: (events: GenericEvent[]) => void, doNotRequestObjects?: boolean): void { + if (!this.handlers.includes(cb)) { + this.handlers.push(cb); + + if (this.handlers.length === 1 && this.connected) { + this.socket + .subscribeObject(this.root ? `${this.root}.*` : '*', this.objectChangeHandler) + .then(() => { + if (!doNotRequestObjects) { + // read all hosts anew and inform about it + void this.getObjects(true).then(objects => { + if (objects) { + Object.keys(objects).forEach(id => + this.objectChangeHandler(id as GetRootFromType, objects[id]), + ); + } + }); + } + }) + .catch(e => window.alert(`Cannot subscribe on objects "${this.root}": ${e}`)); + } + } + } + + unregisterHandler(cb: (events: GenericEvent[]) => void): void { + const pos = this.handlers.indexOf(cb); + if (pos !== -1) { + this.handlers.splice(pos, 1); + if (!this.handlers.length && this.connected) { + this.socket + .unsubscribeObject(this.root ? `${this.root}.*` : '*', this.objectChangeHandler) + .catch(e => window.alert(`Cannot unsubscribe from objects "${this.root}": ${e}`)); + } + } + } + + destroy(): void { + this.handlers.forEach(cb => this.unregisterHandler(cb)); + this.socket.unregisterConnectionHandler(this.connectionHandler); + } +} diff --git a/packages/admin/src-admin/src/Workers/HostAdapterWorker.tsx b/packages/admin/src-admin/src/Workers/HostAdapterWorker.tsx new file mode 100644 index 000000000..4b3c2f51c --- /dev/null +++ b/packages/admin/src-admin/src/Workers/HostAdapterWorker.tsx @@ -0,0 +1,28 @@ +import { type AdminConnection } from '@iobroker/adapter-react-v5'; +import GenericWorker, { type EventType, type GenericEvent } from './GenericWorker'; + +export type HostAdapterEventType = EventType; + +export type HostAdapterEvent = GenericEvent<'adapter'>; + +export default class HostAdapterWorker extends GenericWorker<'adapter'> { + private readonly host: string; + private readonly prefix: string; + + constructor(socket: AdminConnection, host: string) { + super(socket, `${host}.adapter`, 'adapter'); + this.prefix = `${host}.adapter.`; + this.host = host; + } + + getObject(adapterName: string): ioBroker.AdapterObject | null { + if (!this.objects) { + return null; + } + return this.objects[this.prefix + adapterName] || null; + } + + getHost(): string { + return this.host; + } +} diff --git a/packages/admin/src-admin/src/Workers/HostsWorker.tsx b/packages/admin/src-admin/src/Workers/HostsWorker.tsx index ff4796ef2..69ebaf71e 100644 --- a/packages/admin/src-admin/src/Workers/HostsWorker.tsx +++ b/packages/admin/src-admin/src/Workers/HostsWorker.tsx @@ -1,16 +1,12 @@ import type { AdminConnection } from '@iobroker/adapter-react-v5'; import type { FilteredNotificationInformation } from '@iobroker/socket-client'; +import GenericWorker, { type EventType, type GenericEvent } from './GenericWorker'; -export type HostEventType = 'new' | 'changed' | 'deleted'; +export type HostEventType = EventType; export type NotificationAnswer = { result: FilteredNotificationInformation } | null; -export interface HostEvent { - id: `system.host.${string}`; - obj?: ioBroker.HostObject; - type: HostEventType; - oldObj?: ioBroker.HostObject; -} +export type HostEvent = GenericEvent<'host'>; export interface HostAliveEvent { id: `system.host.${string}`; @@ -18,92 +14,27 @@ export interface HostAliveEvent { type: HostEventType; } -export default class HostsWorker { - private readonly socket: AdminConnection; - - private readonly handlers: ((events: HostEvent[]) => void)[]; - - private readonly aliveHandlers: (((events: HostAliveEvent[]) => void) | false)[]; - - private readonly notificationsHandlers: ((notifications: Record) => void)[]; - - private promise: Promise> | null; +export default class HostsWorker extends GenericWorker<'host'> { + private readonly aliveHandlers: (((events: HostAliveEvent[]) => void) | false)[] = []; - private connected: boolean; + private readonly notificationsHandlers: ((notifications: Record) => void)[] = []; - private objects: Record | null; + private readonly aliveStates: Record = {}; - private readonly aliveStates: Record; + private readonly notificationPromises: Record>> = {}; - private readonly notificationPromises: Record>>; - - private notificationTimer: ReturnType | null; + private notificationTimer: ReturnType | null = null; private subscribeTs: number | undefined; constructor(socket: AdminConnection) { - this.socket = socket; - this.handlers = []; + super(socket, 'system.host', 'host'); this.aliveHandlers = []; this.notificationsHandlers = []; - this.promise = null; this.notificationPromises = {}; - - socket.registerConnectionHandler(this.connectionHandler); - - this.connected = this.socket.isConnected(); - console.log(`Connected: ${this.connected}`); - this.objects = {}; this.aliveStates = {}; - if (this.connected) { - this.connectionHandler(true); - } } - objectChangeHandler = (id: string, obj: ioBroker.HostObject): void => { - // if host - if (id.startsWith('system.host.')) { - let type: HostEventType; - let oldObj: ioBroker.HostObject | undefined; - if (obj) { - if (obj.type !== 'host') { - return; - } - - if (this.objects[id]) { - if (JSON.stringify(this.objects[id]) !== JSON.stringify(obj)) { - type = 'changed'; - this.objects[id] = obj; - } else { - // no changes - return; - } - } else { - type = 'new'; - this.objects[id] = obj; - } - } else if (this.objects[id]) { - type = 'deleted'; - oldObj = this.objects[id]; - delete this.objects[id]; - } else { - // deleted unknown instance - return; - } - - this.handlers.forEach(cb => - cb([ - { - id: id as `system.host.${string}`, - obj, - type, - oldObj, - }, - ]), - ); - } - }; - aliveChangeHandler = (id: string, state: ioBroker.State): void => { // if instance if (id.startsWith('system.host.') && id.endsWith('.alive')) { @@ -143,68 +74,14 @@ export default class HostsWorker { } }; - getHosts(update?: boolean): Promise> { - if (!update && this.promise instanceof Promise) { - return this.promise; - } - - this.promise = this.socket - .getHosts(update) - .then(objects => { - this.objects = {}; - objects.forEach(obj => (this.objects[obj._id] = obj)); - return this.objects; - }) - .catch(e => window.alert(`Cannot get hosts: ${e}`)); - - return this.promise; - } - - connectionHandler = (isConnected: boolean): void => { - if (isConnected && !this.connected) { - this.connected = true; - - if (this.handlers.length) { - this.socket - .subscribeObject('system.host.*', this.objectChangeHandler) - .catch(e => window.alert(`Cannot subscribe on object: ${e}`)); - - // read all hosts anew and inform about it - void this.getHosts(true).then( - hosts => hosts && Object.keys(hosts).forEach(id => this.objectChangeHandler(id, hosts[id])), - ); - } + connectionPostHandler(isConnected: boolean): void { + if (isConnected) { if (this.aliveHandlers.length) { void this.socket.subscribeState('system.host.*.alive', this.aliveChangeHandler); } - } else if (!isConnected && this.connected) { - this.connected = false; + } else { Object.keys(this.aliveStates).forEach((id: string) => (this.aliveStates[id] = false)); } - }; - - registerHandler(cb: (events: HostEvent[]) => void): void { - if (!this.handlers.includes(cb)) { - this.handlers.push(cb); - - if (this.handlers.length === 1 && this.connected) { - this.socket - .subscribeObject('system.host.*', this.objectChangeHandler) - .catch(e => window.alert(`Cannot subscribe on object: ${e}`)); - } - } - } - - unregisterHandler(cb: (events: HostEvent[]) => void): void { - const pos = this.handlers.indexOf(cb); - if (pos !== -1) { - this.handlers.splice(pos, 1); - if (!this.handlers.length && this.connected) { - this.socket - .unsubscribeObject('system.host.*', this.objectChangeHandler) - .catch(e => window.alert(`Cannot subscribe on object: ${e}`)); - } - } } registerAliveHandler(cb: (events: HostAliveEvent[]) => void): void { diff --git a/packages/admin/src-admin/src/Workers/InstancesWorker.tsx b/packages/admin/src-admin/src/Workers/InstancesWorker.tsx index 39df0abe9..1ea89b11e 100644 --- a/packages/admin/src-admin/src/Workers/InstancesWorker.tsx +++ b/packages/admin/src-admin/src/Workers/InstancesWorker.tsx @@ -1,160 +1,12 @@ import { type AdminConnection } from '@iobroker/adapter-react-v5'; -import AdminUtils from '../helpers/AdminUtils'; +import GenericWorker, { type EventType, type GenericEvent } from './GenericWorker'; -export type InstanceEventType = 'new' | 'changed' | 'deleted'; +export type InstanceEventType = EventType; -export interface InstanceEvent { - id: string; - obj?: ioBroker.InstanceObject; - type: InstanceEventType; - oldObj?: ioBroker.InstanceObject; -} - -export default class InstancesWorker { - private readonly socket: AdminConnection; - - private readonly handlers: ((events: InstanceEvent[]) => void)[]; - - private promise: Promise> | null; - - private forceUpdate: boolean; - - private connected: boolean; - - private objects: Record | null; +export type InstanceEvent = GenericEvent<'instance'>; +export default class InstancesWorker extends GenericWorker<'instance'> { constructor(socket: AdminConnection) { - this.socket = socket; - this.handlers = []; - this.promise = null; - this.forceUpdate = false; - - socket.registerConnectionHandler(this.connectionHandler); - - this.connected = this.socket.isConnected(); - - this.objects = null; - } - - objectChangeHandler = (id: string, obj?: ioBroker.InstanceObject): void => { - this.objects = this.objects || {}; - // if instance - if (id.match(/^system\.adapter\.[^.]+\.\d+$/)) { - let type: InstanceEventType; - let oldObj: ioBroker.InstanceObject | undefined; - if (obj) { - if (obj.type !== 'instance') { - return; - } - - AdminUtils.fixAdminUI(obj); - - if (this.objects[id]) { - oldObj = this.objects[id]; - if (JSON.stringify(this.objects[id]) !== JSON.stringify(obj)) { - type = 'changed'; - this.objects[id] = obj; - } else { - // no changes - return; - } - } else { - type = 'new'; - this.objects[id] = obj; - } - } else if (this.objects[id]) { - oldObj = this.objects[id]; - type = 'deleted'; - delete this.objects[id]; - } else { - // deleted unknown instance - return; - } - - this.promise = null; - this.socket.getAdapterInstancesResetCache(''); - this.forceUpdate = true; - - this.handlers.forEach(cb => - cb([ - { - id, - obj, - type, - oldObj, - }, - ]), - ); - } - }; - - isForceUpdate(): boolean { - return this.forceUpdate; - } - - // be careful with this object. Do not change them. - getInstances(update?: boolean): Promise> { - if (!update && this.promise instanceof Promise) { - return this.promise; - } - - update = update || this.forceUpdate; - this.forceUpdate = false; - - this.promise = this.socket - .getAdapterInstances('', update) - .then(objects => { - this.objects = {}; - objects.forEach(obj => (this.objects[obj._id] = obj)); - return this.objects; - }) - .catch(e => window.alert(`Cannot get adapter instances: ${e}`)); - - return this.promise; - } - - connectionHandler = (isConnected: boolean): void => { - if (isConnected && !this.connected) { - this.connected = true; - - if (this.handlers.length) { - this.socket - .subscribeObject('system.adapter.*', this.objectChangeHandler) - .catch(e => window.alert(`Cannot subscribe on object: ${e}`)); - - void this.getInstances(true).then( - instances => - instances && Object.keys(instances).forEach(id => this.objectChangeHandler(id, instances[id])), - ); - } - } else if (!isConnected && this.connected) { - this.connected = false; - } - }; - - registerHandler(cb: (events: InstanceEvent[]) => void, doNotRequestAdapters?: boolean): void { - if (!this.handlers.includes(cb)) { - this.handlers.push(cb); - - if (this.handlers.length === 1 && this.connected) { - this.socket - .subscribeObject('system.adapter.*', this.objectChangeHandler) - .then(() => !doNotRequestAdapters && this.getInstances()) - .catch(e => window.alert(`Cannot subscribe on object: ${e}`)); - } - } - } - - unregisterHandler(cb: (events: InstanceEvent[]) => void): void { - const pos = this.handlers.indexOf(cb); - if (pos !== -1) { - this.handlers.splice(pos, 1); - } - - if (!this.handlers.length && this.connected) { - this.socket - .unsubscribeObject('system.adapter.*', this.objectChangeHandler) - .catch(e => window.alert(`Cannot subscribe on object: ${e}`)); - } + super(socket, 'system.adapter', 'instance'); } } diff --git a/packages/admin/src-admin/src/Workers/ObjectsWorker.tsx b/packages/admin/src-admin/src/Workers/ObjectsWorker.tsx index 952e9b257..f72e91cc2 100644 --- a/packages/admin/src-admin/src/Workers/ObjectsWorker.tsx +++ b/packages/admin/src-admin/src/Workers/ObjectsWorker.tsx @@ -10,12 +10,6 @@ export interface ObjectEvent { oldObj?: ioBroker.Object; } -// export interface ObjectsWorker { -// getObjects(update?: boolean): Promise>; -// registerHandler(cb: (events: ObjectEvent[]) => void): void; -// unregisterHandler(cb: (events: ObjectEvent[]) => void, doNotUnsubscribe?: boolean): void; -// } - export default class ObjectsWorker { private readonly socket: AdminConnection; diff --git a/packages/admin/src-admin/src/components/Adapters/AdapterGeneric.tsx b/packages/admin/src-admin/src/components/Adapters/AdapterGeneric.tsx index c5ef3dd89..beb03af32 100644 --- a/packages/admin/src-admin/src/components/Adapters/AdapterGeneric.tsx +++ b/packages/admin/src-admin/src/components/Adapters/AdapterGeneric.tsx @@ -23,7 +23,7 @@ import { Refresh as RefreshIcon, Add as AddIcon, Help as HelpIcon, - KeyboardArrowUp as UpdateSettingsIcon, + KeyboardArrowUp, Cloud as CloudIcon, CloudOff as CloudOffIcon, ArrowUpward as ArrowUpwardIcon, @@ -52,7 +52,7 @@ import AdapterInstallDialog, { type AdapterRatingInfo, type AdaptersContext, } from '@/components/Adapters/AdapterInstallDialog'; -import AutoUpgradeConfigDialog from '@/dialogs/AutoUpgradeConfigDialog'; +import AutoUpgradeConfigDialog, { ICONS } from '@/dialogs/AutoUpgradeConfigDialog'; import IsVisible from '../IsVisible'; import { extractUrlLink } from './Utils'; @@ -258,19 +258,24 @@ export default abstract class AdapterGeneric< } renderAutoUpgradeButton(): JSX.Element | null { - if (!this.installedVersion) { - return null; + const adapterObj = this.props.context.hostAdapterWorker.getObject(this.props.adapterName); + if (!adapterObj) { + return
; } + + // 'none' | 'patch' | 'minor' | 'major' + const autoUpgrade: ioBroker.AutoUpgradePolicy = adapterObj.common?.automaticUpgrade; + return ( this.setState({ autoUpgradeDialogOpen: true, showDialog: true })} > - + {!autoUpgrade || autoUpgrade === 'none' ? : ICONS[autoUpgrade]} ); diff --git a/packages/admin/src-admin/src/components/Adapters/AdapterInstallDialog.tsx b/packages/admin/src-admin/src/components/Adapters/AdapterInstallDialog.tsx index 370ad0481..f882a520f 100644 --- a/packages/admin/src-admin/src/components/Adapters/AdapterInstallDialog.tsx +++ b/packages/admin/src-admin/src/components/Adapters/AdapterInstallDialog.tsx @@ -11,6 +11,7 @@ import type { AdapterInformation } from '@iobroker/js-controller-common-db/build import type InstancesWorker from '@/Workers/InstancesWorker'; import type HostsWorker from '@/Workers/HostsWorker'; import type { RatingDialogRepository } from '@/dialogs/RatingDialog'; +import type HostAdapterWorker from '@/Workers/HostAdapterWorker'; import { extractUrlLink, type RepoAdapterObject } from './Utils'; export type AdapterRating = { @@ -76,6 +77,7 @@ export type AdaptersContext = { isTileView: boolean; updateRating: (adapter: string, rating: RatingDialogRepository) => void; setAdminUpgradeTo: (version: string) => void; + hostAdapterWorker: HostAdapterWorker; }; export interface AdapterInstallDialogState { @@ -163,7 +165,7 @@ export default abstract class AdapterInstallDialog { // new host detected changed = true; hosts.push({ - _id: event.id, + _id: event.id as `system.host.${string}`, common: { name: event.obj.common?.name || '', color: event.obj.common?.color || '', diff --git a/packages/admin/src-admin/src/dialogs/AddInstanceDialog.tsx b/packages/admin/src-admin/src/dialogs/AddInstanceDialog.tsx index 9dfda3710..db76aed4f 100644 --- a/packages/admin/src-admin/src/dialogs/AddInstanceDialog.tsx +++ b/packages/admin/src-admin/src/dialogs/AddInstanceDialog.tsx @@ -144,7 +144,7 @@ class AddInstanceDialog extends Component) => { + void this.props.instancesWorker.getObjects().then((instances: Record) => { const instanceNumbers = Object.keys(instances) .filter(id => instances[id]?.common?.name === this.props.adapter) .map(id => id.substring(id.lastIndexOf('.') + 1)); diff --git a/packages/admin/src-admin/src/dialogs/AutoUpgradeConfigDialog.tsx b/packages/admin/src-admin/src/dialogs/AutoUpgradeConfigDialog.tsx index f2476c770..d685f87db 100644 --- a/packages/admin/src-admin/src/dialogs/AutoUpgradeConfigDialog.tsx +++ b/packages/admin/src-admin/src/dialogs/AutoUpgradeConfigDialog.tsx @@ -1,13 +1,26 @@ import React from 'react'; import { Button, Dialog, DialogActions, DialogContent, DialogTitle, MenuItem, Select, Typography } from '@mui/material'; -import { Close as CloseIcon } from '@mui/icons-material'; +import { + Close as CloseIcon, HorizontalRule, + KeyboardArrowUp, + KeyboardDoubleArrowUp, + North, + VerticalAlignTop +} from "@mui/icons-material"; import { type AdminConnection, I18n, IconCopy as SaveIcon } from '@iobroker/adapter-react-v5'; import { InfoBox } from '@foxriver76/iob-component-lib'; import IsVisible from '@/components/IsVisible'; import { AUTO_UPGRADE_OPTIONS_MAPPING, AUTO_UPGRADE_SETTINGS } from '@/helpers/utils'; +export const ICONS: Record = { + none: , + patch: , + minor: , + major: , +}; + interface AutoUpgradeConfigDialogProps { /** Called when user closes dialog */ onClose: () => void; @@ -24,7 +37,7 @@ interface AutoUpgradeConfigDialogState { currentSavedPolicy: ioBroker.AutoUpgradePolicy; /** The current configured auto upgrade policy */ policy: ioBroker.AutoUpgradePolicy; - /** The repositories the config applies for */ + /** The repositories the config apply for */ repositories: string[]; /** If the feature is supported */ supported: boolean; @@ -106,7 +119,7 @@ export default class AutoUpgradeConfigDialog extends React.Component< obj.common.automaticUpgrade = this.state.policy; await this.props.socket.setObject(this.getAdapterId(), obj); - this.setState({ currentSavedPolicy: this.state.policy }); + this.setState({ currentSavedPolicy: this.state.policy }, () => this.props.onClose()); } /** @@ -145,7 +158,10 @@ export default class AutoUpgradeConfigDialog extends React.Component< key={option} value={option} > - {AUTO_UPGRADE_OPTIONS_MAPPING[option]} +
+ {ICONS[option]} + {AUTO_UPGRADE_OPTIONS_MAPPING[option]} +
))} @@ -173,9 +189,7 @@ export default class AutoUpgradeConfigDialog extends React.Component<