Skip to content

Commit 636e8b0

Browse files
committed
improved file table
1 parent e9e605b commit 636e8b0

File tree

9 files changed

+167
-74
lines changed

9 files changed

+167
-74
lines changed

package-lock.json

Lines changed: 14 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
},
3535
"dependencies": {
3636
"@electron/remote": "1.1.0",
37+
"@swimlane/ngx-datatable": "20.0.0",
3738
"@webosose/ares-cli": "2.1.0",
3839
"async-lock": "1.3.0",
3940
"electron-dl": "3.2.1",

src/app/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {SharedModule} from './shared/shared.module';
2929
import {UpdateDetailsComponent} from './update-details/update-details.component';
3030
import {AttrsPermissionsPipe} from './shared/pipes/attrs-permissions.pipe';
3131
import {NgxFilesizeModule} from 'ngx-filesize';
32+
import {NgxDatatableModule} from '@swimlane/ngx-datatable';
3233

3334
// AoT requires an exported function for factories
3435
export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
@@ -65,6 +66,7 @@ export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
6566
ReactiveFormsModule,
6667
NgbModule,
6768
NgLetModule,
69+
NgxDatatableModule,
6870
TranslateModule.forRoot({
6971
loader: {
7072
provide: TranslateLoader,

src/app/core/services/device-manager.service.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,18 @@ export class DeviceManagerService {
200200
});
201201
}
202202

203+
async newSession2(name: string): Promise<NovacomSession> {
204+
return new Promise<NovacomSession>((resolve, reject) => {
205+
const session: any = new this.novacom.Session(name, (error: any) => {
206+
if (error) {
207+
reject(error);
208+
} else {
209+
resolve(new NovacomSession(session as Session));
210+
}
211+
});
212+
});
213+
}
214+
203215
async sftpSession(name: string): Promise<SFTPSession> {
204216
return this.newSession(name).then(session => new Promise((resolve, reject) => {
205217
session.ssh.sftp((err, sftp) => {
@@ -291,6 +303,26 @@ export class CrashReport {
291303
}
292304
}
293305

306+
export class NovacomSession {
307+
constructor(private session: Session) {
308+
}
309+
310+
public async get(inPath: string, outPath: string): Promise<void> {
311+
return new Promise<void>((resolve, reject) => this.session.get(inPath, outPath, (err, result) => {
312+
if (err) {
313+
reject(err);
314+
} else {
315+
resolve(result);
316+
}
317+
}));
318+
}
319+
320+
public end() {
321+
this.session.end();
322+
cleanupSession();
323+
}
324+
}
325+
294326
export class SFTPSession {
295327
constructor(private sftp: SFTPWrapper) {
296328
}

src/app/home/files/files.component.html

Lines changed: 37 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,42 +6,45 @@
66
<form class="form-inline">
77
</form>
88
</nav>
9-
<div class="w-100 h-100 flex-fill overflow-auto">
10-
<table class="files-table table table-striped">
11-
<thead>
12-
<tr>
13-
<th>Name</th>
14-
<th>Size</th>
15-
<th>Permission</th>
16-
<th>Owner</th>
17-
</tr>
18-
</thead>
19-
<tbody>
20-
<tr *ngFor="let file of (files$ | async)" (click)="onselect(file)">
21-
<td class="name-col text-truncate">
22-
<span class="pr-2" [ngSwitch]="file.type">
23-
<i class="bi-folder-fill" *ngSwitchCase="'dir'"></i>
24-
<i class="bi-file-earmark-fill" *ngSwitchCase="'file'"></i>
25-
<i class="bi-gear-fill" *ngSwitchCase="'device'"></i>
26-
<i class="bi-file-earmark-x-fill" *ngSwitchCase="'invalid'"></i>
27-
<i class="bi-file-earmark-fill" *ngSwitchDefault></i>
28-
</span>
29-
<span>{{file.filename}}</span>
30-
<span *ngIf="file.link" class="text-secondary">
31-
<i class="bi-arrow-right-short px-1"></i>
32-
{{file.link.target}}
33-
</span>
34-
</td>
35-
<td class="property-col text-nowrap">
9+
<div class="w-100 h-100 flex-fill position-relative">
10+
<ngx-datatable class="files-table bootstrap position-absolute" [rows]="(files$ | async) || []" [columns]="columns"
11+
[rowHeight]="40" scrollbarH="true" scrollbarV="true" [sortType]="SortType.single"
12+
[selectionType]="SelectionType.single"
13+
(activate)="itemActivated($event.row, $event.type)">
14+
<ngx-datatable-column name="Name" width="400" [sortable]="true" [canAutoResize]="true" [comparator]="compareName">
15+
<ng-template let-file="row" ngx-datatable-cell-template>
16+
<div class="text-nowrap">
17+
<span class="pr-2" [ngSwitch]="file.type">
18+
<i class="bi-folder-fill" *ngSwitchCase="'dir'"></i>
19+
<i class="bi-file-earmark-fill" *ngSwitchCase="'file'"></i>
20+
<i class="bi-gear-fill" *ngSwitchCase="'device'"></i>
21+
<i class="bi-file-earmark-x-fill" *ngSwitchCase="'invalid'"></i>
22+
<i class="bi-file-earmark-fill" *ngSwitchDefault></i>
23+
</span>
24+
<span>{{file.filename}}</span>
25+
<span *ngIf="file.link" class="text-secondary">
26+
<i class="bi-arrow-right-short px-1"></i>
27+
{{file.link.target}}
28+
</span>
29+
</div>
30+
</ng-template>
31+
</ngx-datatable-column>
32+
<ngx-datatable-column name="Size" [sortable]="false" [canAutoResize]="true" [comparator]="compareSize">
33+
<ng-template let-file="row" ngx-datatable-cell-template>
3634
<span *ngIf="file.type === 'file'">{{file.attrs?.size | filesize:sizeOptions}}</span>
37-
</td>
38-
<td class="property-col text-nowrap">
35+
</ng-template>
36+
</ngx-datatable-column>
37+
<ngx-datatable-column name="Permission" [sortable]="false" [canAutoResize]="true">
38+
<ng-template let-file="row" ngx-datatable-cell-template>
3939
{{file.attrs?.mode | attrsPermissions}}
40-
</td>
41-
<td class="property-col text-nowrap">{{file.attrs?.uid}}</td>
42-
</tr>
43-
</tbody>
44-
</table>
40+
</ng-template>
41+
</ngx-datatable-column>
42+
<ngx-datatable-column name="Owner" [sortable]="false" [canAutoResize]="true">
43+
<ng-template let-file="row" ngx-datatable-cell-template>
44+
{{file.attrs?.uid}}
45+
</ng-template>
46+
</ngx-datatable-column>
47+
</ngx-datatable>
4548
</div>
4649
<div class="stat-bar">
4750
<nav class="h-100 overflow-hidden float-right" aria-label="breadcrumb">

src/app/home/files/files.component.scss

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,14 @@
2626
}
2727

2828
.files-table {
29-
overflow-y: auto;
30-
table-layout: fixed;
31-
32-
.name-col {
33-
overflow: hidden;
34-
text-overflow: ellipsis;
35-
width: auto;
36-
}
37-
38-
.property-col {
39-
width: 50px;
40-
}
41-
42-
td {
43-
vertical-align: middle;
44-
padding-top: 5px;
45-
padding-bottom: 5px;
29+
left: 0;
30+
right: 0;
31+
top: 0;
32+
bottom: 0;
33+
34+
datatable-body-cell {
35+
padding-top: 0 !important;
36+
padding-bottom: 0 !important;
37+
vertical-align: center;
4638
}
4739
}

src/app/home/files/files.component.ts

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import {Device} from "../../../types/novacom";
44
import {BehaviorSubject, Observable, Subject} from "rxjs";
55
import {Attributes, FileEntry} from 'ssh2-streams';
66
import * as path from 'path';
7+
import * as fs from 'fs';
78
import {MessageDialogComponent} from "../../shared/components/message-dialog/message-dialog.component";
89
import {NgbModal} from "@ng-bootstrap/ng-bootstrap";
10+
import {SelectionType, SortType, TableColumn} from "@swimlane/ngx-datatable";
911

1012
@Component({
1113
selector: 'app-files',
@@ -17,15 +19,20 @@ export class FilesComponent implements OnInit {
1719
pwd: string;
1820
files$: Observable<FileItem[]>;
1921
sizeOptions = {base: 2, standard: "jedec"};
20-
private dialog: Electron.Dialog;
22+
columns: TableColumn[] = [{prop: 'filename', name: 'Name'}];
23+
SortType = SortType;
24+
SelectionType = SelectionType;
25+
private remote: Electron.Remote;
2126
private filesSubject: Subject<FileItem[]>;
27+
private fs: typeof fs;
2228

2329
constructor(
2430
private modalService: NgbModal,
2531
private deviceManager: DeviceManagerService,
2632
private electron: ElectronService,
2733
) {
28-
this.dialog = electron.remote.dialog;
34+
this.remote = electron.remote;
35+
this.fs = electron.fs;
2936
deviceManager.selected$.subscribe((selected) => {
3037
this.device = selected;
3138
this.cd('/media/developer');
@@ -38,6 +45,7 @@ export class FilesComponent implements OnInit {
3845
}
3946

4047
async cd(dir: string): Promise<void> {
48+
if (!this.device) return;
4149
dir = path.normalize(dir);
4250
const sftp = await this.deviceManager.sftpSession(this.device.name);
4351
let list: FileItem[];
@@ -59,28 +67,58 @@ export class FilesComponent implements OnInit {
5967
return;
6068
}
6169
this.pwd = dir;
62-
this.filesSubject.next(list.sort((a, b) => {
63-
const dirDiff = (b.type == 'dir' ? 1000 : 0) - (a.type == 'dir' ? 1000 : 0);
64-
return dirDiff + (a.filename > b.filename ? 1 : -1);
65-
}));
70+
this.filesSubject.next(list.sort(this.compareName.bind(this)));
6671
}
6772

68-
async onselect(file: FileItem): Promise<void> {
69-
if (file.type == 'dir') {
70-
await this.cd(path.resolve(this.pwd, file.filename));
71-
} else if (file.type == 'file') {
72-
const returnValue = await this.dialog.showSaveDialog({defaultPath: file.filename});
73-
if (returnValue.canceled) return;
74-
const sftp = await this.deviceManager.sftpSession(this.device.name);
75-
await sftp.fastGet(file.abspath, returnValue.filePath).finally(() => sftp.end())
76-
.catch((e) => MessageDialogComponent.open(this.modalService, {
77-
title: 'Failed to download file',
78-
message: e.message ?? String(e),
79-
positive: 'OK',
80-
}));
73+
compareName(a: FileItem, b: FileItem): number {
74+
const dirDiff = (b.type == 'dir' ? 1000 : 0) - (a.type == 'dir' ? 1000 : 0);
75+
return dirDiff + (a.filename > b.filename ? 1 : -1);
76+
}
77+
78+
compareSize(a: FileItem, b: FileItem): number {
79+
return (a.type == 'file' ? (a.attrs?.size ?? 0) : 0) - (b.type == 'file' ? (b.attrs.size ?? 0) : 0);
80+
}
81+
82+
async selectItem(file: FileItem): Promise<void> {
83+
84+
}
85+
86+
async openItem(file: FileItem): Promise<void> {
87+
switch (file.type) {
88+
case 'dir': {
89+
await this.cd(path.resolve(this.pwd, file.filename));
90+
break;
91+
}
92+
case 'file': {
93+
return await this.openFile(file);
94+
}
8195
}
8296
}
8397

98+
private async openFile(file: FileItem) {
99+
const tempDir = path.join(this.remote.app.getPath('temp'), `devmgr`);
100+
if (!this.fs.existsSync(tempDir)) {
101+
this.fs.mkdirSync(tempDir);
102+
}
103+
const tempPath = path.join(tempDir, `${Date.now()}_${file.filename}`);
104+
const session = await this.deviceManager.newSession2(this.device.name);
105+
await session.get(file.abspath, tempPath).finally(() => session.end());
106+
await this.remote.shell.openPath(tempPath);
107+
}
108+
109+
private async downloadFile(file: FileItem) {
110+
const returnValue = await this.remote.dialog.showSaveDialog({defaultPath: file.filename});
111+
if (returnValue.canceled) return;
112+
const session = await this.deviceManager.newSession2(this.device.name);
113+
await session.get(file.abspath, returnValue.filePath).finally(() => session.end())
114+
.catch((e) => MessageDialogComponent.open(this.modalService, {
115+
title: 'Failed to download file',
116+
message: e.message ?? String(e),
117+
positive: 'OK',
118+
}));
119+
return;
120+
}
121+
84122
async breadcrumbNav(segs: string[]): Promise<void> {
85123
await this.cd(segs.length > 1 ? path.join('/', ...segs) : '/');
86124
}
@@ -133,13 +171,20 @@ export class FilesComponent implements OnInit {
133171
abspath: path.resolve(dir, file.filename),
134172
};
135173
}
174+
175+
async itemActivated(file: FileItem, type: string): Promise<void> {
176+
switch (type) {
177+
case 'dblclick':
178+
return this.openItem(file);
179+
}
180+
}
136181
}
137182

138183
type FileType = 'file' | 'dir' | 'device' | 'special' | 'invalid';
139184

140185
declare interface FileItem {
141186
filename: string;
142-
attrs: Attributes;
187+
attrs: Attributes | null;
143188
link?: LinkInfo;
144189
type: FileType;
145190
abspath: string;

src/styles.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
@import '~bootstrap/scss/bootstrap';
22
@import "~bootstrap-icons/font/bootstrap-icons.css";
3+
@import '~@swimlane/ngx-datatable/index.css';
4+
@import '~@swimlane/ngx-datatable/assets/icons.css';
5+
@import '~@swimlane/ngx-datatable/themes/bootstrap.css';
36
@import "./styles/no-select.scss";
47
@import "./styles/app-item.scss";
58
@import "./styles/overflow.scss";

src/types/novacom.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,9 @@ export interface Session {
5454

5555
run(cmd: string, stdin: Readable | null, stdout: RunOutput, stderr: RunOutput, next: (error: any, result: any) => void): void;
5656

57+
get(inPath: string, outPath: string, next: (error: any, result: any) => void): void;
58+
59+
put(inPath: string, outPath: string, next: (error: any, result: any) => void): void;
60+
5761
end(): void;
5862
}

0 commit comments

Comments
 (0)