Skip to content

Commit

Permalink
feat: extend configuration history records with device type and name (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
ert78gb authored Jun 26, 2024
1 parent df9af6c commit eeb648a
Show file tree
Hide file tree
Showing 25 changed files with 274 additions and 91 deletions.
2 changes: 1 addition & 1 deletion packages/uhk-agent/src/services/device.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -744,7 +744,7 @@ export class DeviceService {
this._checkStatusBuffer = true;

if (data.saveInHistory) {
await saveUserConfigHistoryAsync(buffer);
await saveUserConfigHistoryAsync(buffer, data.deviceId, data.uniqueId);
await this.loadUserConfigFromHistory(event);
}

Expand Down
25 changes: 12 additions & 13 deletions packages/uhk-agent/src/util/backup-user-confoguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,14 @@ import * as path from 'path';
import {
BackupUserConfiguration,
BackupUserConfigurationInfo,
convertHistoryFilenameToDisplayText,
LogService,
SaveUserConfigurationData,
shouldUpgradeAgent,
UhkBuffer,
UserConfiguration,
VersionInformation
} from 'uhk-common';

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

export const getBackupUserConfigurationPath = (uniqueId: number): string => {
Expand Down Expand Up @@ -49,7 +47,7 @@ export async function getBackupUserConfigurationContent(logService: LogService,
}
}

const fromHistory = await getCompatibleUserConfigFromHistory(logService, versionInformation);
const fromHistory = await getCompatibleUserConfigFromHistory(logService, versionInformation, uniqueId);
if (fromHistory) {
logService.config('Backup user configuration from history', fromHistory.userConfiguration);
return fromHistory;
Expand All @@ -65,17 +63,18 @@ export async function getBackupUserConfigurationContent(logService: LogService,
}
}

export async function getCompatibleUserConfigFromHistory(logService: LogService, versionInformation: VersionInformation): Promise<BackupUserConfiguration> {
let files = await loadUserConfigHistoryAsync();
files = files
.filter(file => path.extname(file) === '.bin')
.sort((a, b) => a.localeCompare(b) * -1);
export async function getCompatibleUserConfigFromHistory(logService: LogService, versionInformation: VersionInformation, uniqueId: number): Promise<BackupUserConfiguration> {
let history = await loadUserConfigHistoryAsync();

const deviceHistory = history.devices.find(device => device.uniqueId === uniqueId);

const files = deviceHistory
? [...deviceHistory.files, ...history.commonFiles]
: history.commonFiles;

for (const file of files) {
try {
const content = await getUserConfigFromHistoryAsync(file);
const userConfig = new UserConfiguration();
userConfig.fromBinary(UhkBuffer.fromArray(content));
const userConfig = await loadUserConfigFromBinaryFile(file.filePath);

if (shouldUpgradeAgent(userConfig.getSemanticVersion(), false, versionInformation?.userConfigVersion)) {
continue;
Expand All @@ -84,7 +83,7 @@ export async function getCompatibleUserConfigFromHistory(logService: LogService,
return {
info: BackupUserConfigurationInfo.EarlierCompatible,
userConfiguration: userConfig.toJsonObject(),
date: convertHistoryFilenameToDisplayText(file)
date: file.timestamp,
};
} catch (error) {
logService.error('Cannot parse backup user config from history', error);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import { readFile } from 'fs';
import { join } from 'path';
import { promisify } from 'util';

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

const readFileAsync = promisify(readFile);
import { readFile } from 'node:fs/promises';

export async function getUserConfigFromHistoryAsync(filename: string): Promise<Array<number>> {
const filePath = join(await getUserConfigHistoryDirAsync(), filename);
const buffer = await readFileAsync(filePath);
const buffer = await readFile(filename);

return [...buffer];
}
1 change: 1 addition & 0 deletions packages/uhk-agent/src/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from './get-updater-logger';
export * from './get-user-config-from-history-async';
export * from './get-user-config-history-dir-async';
export * from './get-window-background-color';
export * from './load-user-config-from-binary-file';
export * from './load-user-config-history-async';
export * from './make-folder-writeable-to-user-on-linux';
export * from './print-usb-devices';
Expand Down
17 changes: 17 additions & 0 deletions packages/uhk-agent/src/util/load-user-config-from-binary-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { readFile } from 'node:fs/promises';
import { UhkBuffer, UserConfiguration } from "uhk-common";

/**
* Load user configuration history from a binary file.
*
* @param filePath - The path to the binary file.
* @returns The user configuration.
*/
export async function loadUserConfigFromBinaryFile(filePath:string): Promise<UserConfiguration> {
const buffer = await readFile(filePath);
const userConfig = new UserConfiguration();

userConfig.fromBinary(UhkBuffer.fromArray([...buffer]));

return userConfig;
}
84 changes: 78 additions & 6 deletions packages/uhk-agent/src/util/load-user-config-history-async.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,84 @@
import { readdir } from 'fs';
import { promisify } from 'util';
import { readdir, stat } from 'node:fs/promises';
import path from 'node:path';
import { convertHistoryFilenameToDisplayText } from 'uhk-common';
import { getMd5HashFromFilename } from 'uhk-common';
import {
DeviceUserConfigHistory,
sortStringDesc,
UHK_DEVICES,
UserConfigHistory,
} from 'uhk-common';

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

const readdirAsync = promisify(readdir);
export async function loadUserConfigHistoryAsync(): Promise<UserConfigHistory> {
const history: UserConfigHistory = {
commonFiles: [],
devices: []
};

export async function loadUserConfigHistoryAsync(): Promise<Array<string>> {
const files = await readdirAsync(await getUserConfigHistoryDirAsync());
const userConfigHistoryDir = await getUserConfigHistoryDirAsync();
const entries = await readdir(userConfigHistoryDir);

return files;
for (const entry of entries) {
const filePath = path.join(userConfigHistoryDir, entry);
const entryStat = await stat(filePath);

if (entryStat.isFile()) {
if (path.extname(entry) === '.bin') {
history.commonFiles.push({
filePath,
md5Hash: getMd5HashFromFilename(entry),
timestamp: convertHistoryFilenameToDisplayText(entry),
});
}
} else if (entryStat.isDirectory()) {
const entrySplit = entry.split('-');

if (entrySplit.length !== 2) {
continue;
}

const deviceId = Number.parseInt(entrySplit[1], 10);

if (isNaN(deviceId)) {
continue;
}

const deviceHistoryDir = path.join(userConfigHistoryDir, entry);
const deviceHistory: DeviceUserConfigHistory = {
uniqueId: Number.parseInt(entrySplit[0], 10),
device: UHK_DEVICES.find(device => device.id === deviceId),
deviceName: '',
files: (await readdir(deviceHistoryDir))
.filter(file => path.extname(file) === '.bin')
.sort(sortStringDesc)
.map(file => {
return {
filePath: path.join(deviceHistoryDir, file),
md5Hash: getMd5HashFromFilename(file),
timestamp: convertHistoryFilenameToDisplayText(file),
};
}),
};

for (const file of deviceHistory.files) {
try {
const userConfig = await loadUserConfigFromBinaryFile(file.filePath);
deviceHistory.deviceName = userConfig.deviceName;
break;
} catch {
// Maybe the user config is newer than Agent supports, or corrupted.
}
}

history.devices.push(deviceHistory);
}
}

history.commonFiles
.sort((a, b) => sortStringDesc(a.timestamp, b.timestamp));

return history;
}
16 changes: 9 additions & 7 deletions packages/uhk-agent/src/util/save-user-config-history-async.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { writeFile } from 'fs';
import { join } from 'path';
import { promisify } from 'util';
import { ensureDir } from 'fs-extra';
import { writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { createMd5Hash, getUserConfigHistoryFilename, Buffer } from 'uhk-common';

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

const writeFileAsync = promisify(writeFile);

export async function saveUserConfigHistoryAsync(buffer: Buffer): Promise<void> {
export async function saveUserConfigHistoryAsync(buffer: Buffer, deviceId: number, uniqueId: number): Promise<void> {
const deviceDir = `${uniqueId}-${deviceId}`;
const deviceDirPath = join(await getUserConfigHistoryDirAsync(), deviceDir);
await ensureDir(deviceDirPath);
const md5Hash = createMd5Hash(buffer);
const filename = getUserConfigHistoryFilename(md5Hash);
const filePath = join(await getUserConfigHistoryDirAsync(), filename);
const filePath = join(deviceDirPath, filename);

return writeFileAsync(filePath, buffer, { encoding: 'ascii' });
return writeFile(filePath, buffer, { encoding: 'ascii' });
}
1 change: 1 addition & 0 deletions packages/uhk-common/src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ export * from './udev-rules-info.js';
export * from './uhk-products.js';
export * from './update-firmware-data.js';
export * from './upload-file-data.js';
export * from './user-config-history.js';
export * from './halves-info.js';
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
export interface SaveUserConfigurationData {
/**
* UHK device product id.
*/
deviceId: number;
/**
* The unique identifier of the UHK keyboard.
*/
uniqueId: number;
configuration: string;
saveInHistory: boolean;
Expand Down
27 changes: 27 additions & 0 deletions packages/uhk-common/src/models/user-config-history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { UhkDeviceProduct } from './uhk-products';

export interface HistoryFileInfo {
filePath: string;
md5Hash: string;
timestamp: string;
}

export interface DeviceUserConfigHistory {
uniqueId: number;
device: UhkDeviceProduct;
/**
* Device name from the latest user configuration.
*/
deviceName: string;
files: HistoryFileInfo[];
}

export interface UserConfigHistory {
/**
* Files in the root of the history directory.
* These files are common for all devices, because we introduced the device specific history directories later.
* We show the common files in the UI for every device.
*/
commonFiles: HistoryFileInfo[];
devices: DeviceUserConfigHistory[];
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export function getMd5HashFromFilename(filename: string): string {
return filename.substr(16, 32);
return filename.substring(16, 48);
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,6 @@
}
}

.nav-tabs > li {
overflow: hidden;
cursor: pointer;

&.disabled {
cursor: not-allowed;
}
}

.arrowCustom {
position: absolute;
top: -15px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,29 @@ <h4 class="panel-title">
Loading...
</div>

<div *ngIf="!state.loading && !state.files.length">
<div *ngIf="!state.loading && !state.tabs.length">
No configurations were saved yet.
</div>

<ul class="list-unstyled mb-0"
*ngIf="!state.loading && state.files.length">
<li *ngFor="let fileInfo of state.files; trackBy:trackByFn"
<ul class="nav nav-tabs">
<li *ngFor="let tab of state.tabs; let index = index"
class="nav-item"
[class.disabled]="tab.disabled"
(click)="onSelectTab(index)">
<a class="nav-link"
[class.active]="selectedTabIndex === index"
[class.disabled]="tab.disabled">
<span>{{ tab.displayText }}</span>
</a>
</li>
</ul>

<ul class="list-unstyled mb-0 mt-2"
*ngIf="!state.loading && state.tabs[selectedTabIndex].files.length">
<li *ngFor="let fileInfo of state.tabs[selectedTabIndex].files; trackBy:trackByFn"
class="history-list-item">
<span class="btn btn-link btn-padding-0 current">
{{ fileInfo.file | userConfigHistory }}
{{ fileInfo.timestamp }}
</span>

<span class="btn btn-link btn-padding-0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ export class UserConfigurationHistoryComponent {

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

selectedTabIndex = 0;

trackByFn(index: number, key: HistoryFileInfo): string {
return key.file;
}

onSelectTab(index: number): void {
this.selectedTabIndex = index;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,19 @@ export interface HistoryFileInfo {
*/
displayText: string;
showRestore: boolean;
/**
* The timestamp of the saved user configuration
*/
timestamp: string;
}

export interface Tab {
displayText: string;
files: HistoryFileInfo[];
}

export interface UserConfigHistoryComponentState {
files: Array<HistoryFileInfo>;
tabs: Tab[];
loading: boolean;
disabled: boolean;
}
1 change: 0 additions & 1 deletion packages/uhk-web/src/app/pipes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,3 @@ export { NewLineToBrPipe } from './new-line-to-br.pipe';
export { SafeHtmlPipe } from './safe-html.pipe';
export { SafeStylePipe } from './safe-style.pipe';
export { SafeUrlPipe } from './safe-url.pipe';
export { UserConfigHistoryDisplayTextPipe } from './user-config-history-display-text.pipe';

This file was deleted.

Loading

0 comments on commit eeb648a

Please sign in to comment.