Skip to content

Commit eeb648a

Browse files
authored
feat: extend configuration history records with device type and name (#2303)
* feat: extend configuration history records with device type and name * feat: save user-config in device specific folder * fix: substr replace * fix: show device name in tab label * fix: tab of the current device * delete UserConfigHistoryDisplayTextPipe * fix: use pointer cursor
1 parent df9af6c commit eeb648a

25 files changed

+274
-91
lines changed

packages/uhk-agent/src/services/device.service.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -744,7 +744,7 @@ export class DeviceService {
744744
this._checkStatusBuffer = true;
745745

746746
if (data.saveInHistory) {
747-
await saveUserConfigHistoryAsync(buffer);
747+
await saveUserConfigHistoryAsync(buffer, data.deviceId, data.uniqueId);
748748
await this.loadUserConfigFromHistory(event);
749749
}
750750

packages/uhk-agent/src/util/backup-user-confoguration.ts

+12-13
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,14 @@ import * as path from 'path';
44
import {
55
BackupUserConfiguration,
66
BackupUserConfigurationInfo,
7-
convertHistoryFilenameToDisplayText,
87
LogService,
98
SaveUserConfigurationData,
109
shouldUpgradeAgent,
11-
UhkBuffer,
1210
UserConfiguration,
1311
VersionInformation
1412
} from 'uhk-common';
1513

16-
import { getUserConfigFromHistoryAsync } from './get-user-config-from-history-async';
14+
import { loadUserConfigFromBinaryFile } from './load-user-config-from-binary-file';
1715
import { loadUserConfigHistoryAsync } from './load-user-config-history-async';
1816

1917
export const getBackupUserConfigurationPath = (uniqueId: number): string => {
@@ -49,7 +47,7 @@ export async function getBackupUserConfigurationContent(logService: LogService,
4947
}
5048
}
5149

52-
const fromHistory = await getCompatibleUserConfigFromHistory(logService, versionInformation);
50+
const fromHistory = await getCompatibleUserConfigFromHistory(logService, versionInformation, uniqueId);
5351
if (fromHistory) {
5452
logService.config('Backup user configuration from history', fromHistory.userConfiguration);
5553
return fromHistory;
@@ -65,17 +63,18 @@ export async function getBackupUserConfigurationContent(logService: LogService,
6563
}
6664
}
6765

68-
export async function getCompatibleUserConfigFromHistory(logService: LogService, versionInformation: VersionInformation): Promise<BackupUserConfiguration> {
69-
let files = await loadUserConfigHistoryAsync();
70-
files = files
71-
.filter(file => path.extname(file) === '.bin')
72-
.sort((a, b) => a.localeCompare(b) * -1);
66+
export async function getCompatibleUserConfigFromHistory(logService: LogService, versionInformation: VersionInformation, uniqueId: number): Promise<BackupUserConfiguration> {
67+
let history = await loadUserConfigHistoryAsync();
68+
69+
const deviceHistory = history.devices.find(device => device.uniqueId === uniqueId);
70+
71+
const files = deviceHistory
72+
? [...deviceHistory.files, ...history.commonFiles]
73+
: history.commonFiles;
7374

7475
for (const file of files) {
7576
try {
76-
const content = await getUserConfigFromHistoryAsync(file);
77-
const userConfig = new UserConfiguration();
78-
userConfig.fromBinary(UhkBuffer.fromArray(content));
77+
const userConfig = await loadUserConfigFromBinaryFile(file.filePath);
7978

8079
if (shouldUpgradeAgent(userConfig.getSemanticVersion(), false, versionInformation?.userConfigVersion)) {
8180
continue;
@@ -84,7 +83,7 @@ export async function getCompatibleUserConfigFromHistory(logService: LogService,
8483
return {
8584
info: BackupUserConfigurationInfo.EarlierCompatible,
8685
userConfiguration: userConfig.toJsonObject(),
87-
date: convertHistoryFilenameToDisplayText(file)
86+
date: file.timestamp,
8887
};
8988
} catch (error) {
9089
logService.error('Cannot parse backup user config from history', error);
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
1-
import { readFile } from 'fs';
2-
import { join } from 'path';
3-
import { promisify } from 'util';
4-
5-
import { getUserConfigHistoryDirAsync } from './get-user-config-history-dir-async';
6-
7-
const readFileAsync = promisify(readFile);
1+
import { readFile } from 'node:fs/promises';
82

93
export async function getUserConfigFromHistoryAsync(filename: string): Promise<Array<number>> {
10-
const filePath = join(await getUserConfigHistoryDirAsync(), filename);
11-
const buffer = await readFileAsync(filePath);
4+
const buffer = await readFile(filename);
125

136
return [...buffer];
147
}

packages/uhk-agent/src/util/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export * from './get-updater-logger';
99
export * from './get-user-config-from-history-async';
1010
export * from './get-user-config-history-dir-async';
1111
export * from './get-window-background-color';
12+
export * from './load-user-config-from-binary-file';
1213
export * from './load-user-config-history-async';
1314
export * from './make-folder-writeable-to-user-on-linux';
1415
export * from './print-usb-devices';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { readFile } from 'node:fs/promises';
2+
import { UhkBuffer, UserConfiguration } from "uhk-common";
3+
4+
/**
5+
* Load user configuration history from a binary file.
6+
*
7+
* @param filePath - The path to the binary file.
8+
* @returns The user configuration.
9+
*/
10+
export async function loadUserConfigFromBinaryFile(filePath:string): Promise<UserConfiguration> {
11+
const buffer = await readFile(filePath);
12+
const userConfig = new UserConfiguration();
13+
14+
userConfig.fromBinary(UhkBuffer.fromArray([...buffer]));
15+
16+
return userConfig;
17+
}
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,84 @@
1-
import { readdir } from 'fs';
2-
import { promisify } from 'util';
1+
import { readdir, stat } from 'node:fs/promises';
2+
import path from 'node:path';
3+
import { convertHistoryFilenameToDisplayText } from 'uhk-common';
4+
import { getMd5HashFromFilename } from 'uhk-common';
5+
import {
6+
DeviceUserConfigHistory,
7+
sortStringDesc,
8+
UHK_DEVICES,
9+
UserConfigHistory,
10+
} from 'uhk-common';
311

412
import { getUserConfigHistoryDirAsync } from './get-user-config-history-dir-async';
13+
import { loadUserConfigFromBinaryFile } from './load-user-config-from-binary-file';
514

6-
const readdirAsync = promisify(readdir);
15+
export async function loadUserConfigHistoryAsync(): Promise<UserConfigHistory> {
16+
const history: UserConfigHistory = {
17+
commonFiles: [],
18+
devices: []
19+
};
720

8-
export async function loadUserConfigHistoryAsync(): Promise<Array<string>> {
9-
const files = await readdirAsync(await getUserConfigHistoryDirAsync());
21+
const userConfigHistoryDir = await getUserConfigHistoryDirAsync();
22+
const entries = await readdir(userConfigHistoryDir);
1023

11-
return files;
24+
for (const entry of entries) {
25+
const filePath = path.join(userConfigHistoryDir, entry);
26+
const entryStat = await stat(filePath);
27+
28+
if (entryStat.isFile()) {
29+
if (path.extname(entry) === '.bin') {
30+
history.commonFiles.push({
31+
filePath,
32+
md5Hash: getMd5HashFromFilename(entry),
33+
timestamp: convertHistoryFilenameToDisplayText(entry),
34+
});
35+
}
36+
} else if (entryStat.isDirectory()) {
37+
const entrySplit = entry.split('-');
38+
39+
if (entrySplit.length !== 2) {
40+
continue;
41+
}
42+
43+
const deviceId = Number.parseInt(entrySplit[1], 10);
44+
45+
if (isNaN(deviceId)) {
46+
continue;
47+
}
48+
49+
const deviceHistoryDir = path.join(userConfigHistoryDir, entry);
50+
const deviceHistory: DeviceUserConfigHistory = {
51+
uniqueId: Number.parseInt(entrySplit[0], 10),
52+
device: UHK_DEVICES.find(device => device.id === deviceId),
53+
deviceName: '',
54+
files: (await readdir(deviceHistoryDir))
55+
.filter(file => path.extname(file) === '.bin')
56+
.sort(sortStringDesc)
57+
.map(file => {
58+
return {
59+
filePath: path.join(deviceHistoryDir, file),
60+
md5Hash: getMd5HashFromFilename(file),
61+
timestamp: convertHistoryFilenameToDisplayText(file),
62+
};
63+
}),
64+
};
65+
66+
for (const file of deviceHistory.files) {
67+
try {
68+
const userConfig = await loadUserConfigFromBinaryFile(file.filePath);
69+
deviceHistory.deviceName = userConfig.deviceName;
70+
break;
71+
} catch {
72+
// Maybe the user config is newer than Agent supports, or corrupted.
73+
}
74+
}
75+
76+
history.devices.push(deviceHistory);
77+
}
78+
}
79+
80+
history.commonFiles
81+
.sort((a, b) => sortStringDesc(a.timestamp, b.timestamp));
82+
83+
return history;
1284
}
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1-
import { writeFile } from 'fs';
2-
import { join } from 'path';
3-
import { promisify } from 'util';
1+
import { ensureDir } from 'fs-extra';
2+
import { writeFile } from 'node:fs/promises';
3+
import { join } from 'node:path';
44
import { createMd5Hash, getUserConfigHistoryFilename, Buffer } from 'uhk-common';
55

66
import { getUserConfigHistoryDirAsync } from './get-user-config-history-dir-async';
77

8-
const writeFileAsync = promisify(writeFile);
98

10-
export async function saveUserConfigHistoryAsync(buffer: Buffer): Promise<void> {
9+
export async function saveUserConfigHistoryAsync(buffer: Buffer, deviceId: number, uniqueId: number): Promise<void> {
10+
const deviceDir = `${uniqueId}-${deviceId}`;
11+
const deviceDirPath = join(await getUserConfigHistoryDirAsync(), deviceDir);
12+
await ensureDir(deviceDirPath);
1113
const md5Hash = createMd5Hash(buffer);
1214
const filename = getUserConfigHistoryFilename(md5Hash);
13-
const filePath = join(await getUserConfigHistoryDirAsync(), filename);
15+
const filePath = join(deviceDirPath, filename);
1416

15-
return writeFileAsync(filePath, buffer, { encoding: 'ascii' });
17+
return writeFile(filePath, buffer, { encoding: 'ascii' });
1618
}

packages/uhk-common/src/models/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ export * from './udev-rules-info.js';
2929
export * from './uhk-products.js';
3030
export * from './update-firmware-data.js';
3131
export * from './upload-file-data.js';
32+
export * from './user-config-history.js';
3233
export * from './halves-info.js';

packages/uhk-common/src/models/save-user-configuration-data.ts

+7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
export interface SaveUserConfigurationData {
2+
/**
3+
* UHK device product id.
4+
*/
5+
deviceId: number;
6+
/**
7+
* The unique identifier of the UHK keyboard.
8+
*/
29
uniqueId: number;
310
configuration: string;
411
saveInHistory: boolean;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { UhkDeviceProduct } from './uhk-products';
2+
3+
export interface HistoryFileInfo {
4+
filePath: string;
5+
md5Hash: string;
6+
timestamp: string;
7+
}
8+
9+
export interface DeviceUserConfigHistory {
10+
uniqueId: number;
11+
device: UhkDeviceProduct;
12+
/**
13+
* Device name from the latest user configuration.
14+
*/
15+
deviceName: string;
16+
files: HistoryFileInfo[];
17+
}
18+
19+
export interface UserConfigHistory {
20+
/**
21+
* Files in the root of the history directory.
22+
* These files are common for all devices, because we introduced the device specific history directories later.
23+
* We show the common files in the UI for every device.
24+
*/
25+
commonFiles: HistoryFileInfo[];
26+
devices: DeviceUserConfigHistory[];
27+
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export function getMd5HashFromFilename(filename: string): string {
2-
return filename.substr(16, 32);
2+
return filename.substring(16, 48);
33
}

packages/uhk-web/src/app/components/popover/popover.component.scss

-9
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,6 @@
2929
}
3030
}
3131

32-
.nav-tabs > li {
33-
overflow: hidden;
34-
cursor: pointer;
35-
36-
&.disabled {
37-
cursor: not-allowed;
38-
}
39-
}
40-
4132
.arrowCustom {
4233
position: absolute;
4334
top: -15px;

packages/uhk-web/src/app/components/user-configuration-history/user-configuration-history.component.html

+18-5
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,29 @@ <h4 class="panel-title">
1010
Loading...
1111
</div>
1212

13-
<div *ngIf="!state.loading && !state.files.length">
13+
<div *ngIf="!state.loading && !state.tabs.length">
1414
No configurations were saved yet.
1515
</div>
1616

17-
<ul class="list-unstyled mb-0"
18-
*ngIf="!state.loading && state.files.length">
19-
<li *ngFor="let fileInfo of state.files; trackBy:trackByFn"
17+
<ul class="nav nav-tabs">
18+
<li *ngFor="let tab of state.tabs; let index = index"
19+
class="nav-item"
20+
[class.disabled]="tab.disabled"
21+
(click)="onSelectTab(index)">
22+
<a class="nav-link"
23+
[class.active]="selectedTabIndex === index"
24+
[class.disabled]="tab.disabled">
25+
<span>{{ tab.displayText }}</span>
26+
</a>
27+
</li>
28+
</ul>
29+
30+
<ul class="list-unstyled mb-0 mt-2"
31+
*ngIf="!state.loading && state.tabs[selectedTabIndex].files.length">
32+
<li *ngFor="let fileInfo of state.tabs[selectedTabIndex].files; trackBy:trackByFn"
2033
class="history-list-item">
2134
<span class="btn btn-link btn-padding-0 current">
22-
{{ fileInfo.file | userConfigHistory }}
35+
{{ fileInfo.timestamp }}
2336
</span>
2437

2538
<span class="btn btn-link btn-padding-0"

packages/uhk-web/src/app/components/user-configuration-history/user-configuration-history.component.ts

+6
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@ export class UserConfigurationHistoryComponent {
1313

1414
@Output() getUserConfigFromHistory = new EventEmitter<string>();
1515

16+
selectedTabIndex = 0;
17+
1618
trackByFn(index: number, key: HistoryFileInfo): string {
1719
return key.file;
1820
}
21+
22+
onSelectTab(index: number): void {
23+
this.selectedTabIndex = index;
24+
}
1925
}

packages/uhk-web/src/app/models/user-config-history-component-state.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,19 @@ export interface HistoryFileInfo {
1111
*/
1212
displayText: string;
1313
showRestore: boolean;
14+
/**
15+
* The timestamp of the saved user configuration
16+
*/
17+
timestamp: string;
18+
}
19+
20+
export interface Tab {
21+
displayText: string;
22+
files: HistoryFileInfo[];
1423
}
1524

1625
export interface UserConfigHistoryComponentState {
17-
files: Array<HistoryFileInfo>;
26+
tabs: Tab[];
1827
loading: boolean;
1928
disabled: boolean;
2029
}

packages/uhk-web/src/app/pipes/index.ts

-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,3 @@ export { NewLineToBrPipe } from './new-line-to-br.pipe';
33
export { SafeHtmlPipe } from './safe-html.pipe';
44
export { SafeStylePipe } from './safe-style.pipe';
55
export { SafeUrlPipe } from './safe-url.pipe';
6-
export { UserConfigHistoryDisplayTextPipe } from './user-config-history-display-text.pipe';

packages/uhk-web/src/app/pipes/user-config-history-display-text.pipe.ts

-12
This file was deleted.

0 commit comments

Comments
 (0)