mirror of
https://github.com/bitwarden/browser
synced 2026-02-12 06:23:38 +00:00
wip files demo
This commit is contained in:
@@ -4903,5 +4903,8 @@
|
||||
},
|
||||
"extraWide": {
|
||||
"message": "Extra wide"
|
||||
},
|
||||
"files": {
|
||||
"message": "Files"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,10 +149,21 @@ export class ProductSwitcherService {
|
||||
external: true,
|
||||
},
|
||||
isActive:
|
||||
!this.router.url.includes("/files") &&
|
||||
!this.router.url.includes("/sm/") &&
|
||||
!this.router.url.includes("/organizations/") &&
|
||||
!this.router.url.includes("/providers/"),
|
||||
},
|
||||
files: {
|
||||
name: "File Manager",
|
||||
icon: "bwi-file",
|
||||
appRoute: "/files",
|
||||
marketingRoute: {
|
||||
route: "https://bitwarden.com/products/personal/",
|
||||
external: true,
|
||||
},
|
||||
isActive: this.router.url.includes("/files"),
|
||||
},
|
||||
sm: {
|
||||
name: "Secrets Manager",
|
||||
icon: "bwi-cli",
|
||||
@@ -196,7 +207,7 @@ export class ProductSwitcherService {
|
||||
},
|
||||
} satisfies Record<string, ProductSwitcherItem>;
|
||||
|
||||
const bento: ProductSwitcherItem[] = [products.pm];
|
||||
const bento: ProductSwitcherItem[] = [products.pm, products.files];
|
||||
const other: ProductSwitcherItem[] = [];
|
||||
|
||||
if (smOrg) {
|
||||
|
||||
@@ -87,6 +87,8 @@ import { ReportsModule } from "./tools/reports";
|
||||
import { AccessComponent } from "./tools/send/access.component";
|
||||
import { SendAccessExplainerComponent } from "./tools/send/send-access-explainer.component";
|
||||
import { SendComponent } from "./tools/send/send.component";
|
||||
import { FilesLayoutComponent } from "./vault/files/files-layout.component";
|
||||
import { FilesComponent } from "./vault/files/files.component";
|
||||
import { VaultModule } from "./vault/individual-vault/vault.module";
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -663,6 +665,17 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "files",
|
||||
component: FilesLayoutComponent,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: FilesComponent,
|
||||
data: { titleId: "files" } satisfies RouteDataProperties,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: UserLayoutComponent,
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<bit-dialog [title]="data.file.attachment.fileName" size="large" background="alt">
|
||||
<ng-container bitDialogContent>
|
||||
<ng-container *ngIf="fileType === 'image'">
|
||||
<img *ngIf="srcUrl" [src]="srcUrl" class="tw-w-full tw-h-full" />
|
||||
<i *ngIf="!srcUrl" class="bwi bwi-spinner bwi-spin"></i>
|
||||
</ng-container>
|
||||
|
||||
<!-- FIXME: doesn't work -->
|
||||
<ng-container *ngIf="fileType === 'text' && blob">
|
||||
<pre>
|
||||
{{ blob.text() | async }}
|
||||
</pre
|
||||
>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="button" bitIconButton="bwi-download"></button>
|
||||
<button type="button" bitIconButton="bwi-star"></button>
|
||||
<button class="tw-ml-auto" type="button" bitIconButton="bwi-trash" buttonType="danger"></button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
@@ -0,0 +1,62 @@
|
||||
import { DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, OnInit, inject } from "@angular/core";
|
||||
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
DialogService,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { SharedModule } from "../../shared";
|
||||
|
||||
import { FileService } from "./file.service";
|
||||
import { FileView } from "./files.component";
|
||||
|
||||
export type FilePreviewDialogData = {
|
||||
file: FileView;
|
||||
};
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
templateUrl: "file-preview-dialog.component.html",
|
||||
imports: [SharedModule, DialogModule, ButtonModule, AsyncActionsModule],
|
||||
})
|
||||
export class FilePreviewDialogComponent implements OnInit {
|
||||
protected data = inject(DIALOG_DATA) as FilePreviewDialogData;
|
||||
|
||||
private fileService = inject(FileService);
|
||||
|
||||
protected srcUrl?: string;
|
||||
protected blob?: Blob;
|
||||
|
||||
protected get fileType(): string {
|
||||
return this.fileService.getFileType(this.data.file);
|
||||
}
|
||||
|
||||
protected get icon(): `bwi-${string}` {
|
||||
switch (this.fileType) {
|
||||
case "image":
|
||||
return "bwi-camera";
|
||||
case "pdf":
|
||||
return "bwi-pdf";
|
||||
case "text":
|
||||
return "bwi-file-text";
|
||||
default:
|
||||
return "bwi-file";
|
||||
}
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.blob = await this.fileService.toBlob(this.data.file);
|
||||
this.srcUrl = URL.createObjectURL(this.blob);
|
||||
}
|
||||
|
||||
static open(dialogService: DialogService, file: FileView) {
|
||||
dialogService.open(FilePreviewDialogComponent, {
|
||||
data: {
|
||||
file,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
94
apps/web/src/app/vault/files/file.service.ts
Normal file
94
apps/web/src/app/vault/files/file.service.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { Injectable, inject } from "@angular/core";
|
||||
import { Observable, from, map, shareReplay } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { SubscriptionResponse } from "@bitwarden/common/billing/models/response/subscription.response";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
export type FileView = {
|
||||
cipher: CipherView;
|
||||
attachment: AttachmentView;
|
||||
};
|
||||
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class FileService {
|
||||
private apiService = inject(ApiService);
|
||||
private cipherService = inject(CipherService);
|
||||
private encryptService = inject(EncryptService);
|
||||
private keyService = inject(KeyService);
|
||||
|
||||
files$: Observable<FileView[]> = this.cipherService.cipherViews$.pipe(
|
||||
map((ciphers) =>
|
||||
Object.values(ciphers)
|
||||
.filter((cipher) => Array.isArray(cipher.attachments) && cipher.attachments.length > 0)
|
||||
.map((cipher) => cipher.attachments.map((attachment) => ({ cipher, attachment })))
|
||||
.flat(),
|
||||
),
|
||||
);
|
||||
|
||||
sub$: Observable<SubscriptionResponse> = from(this.apiService.getUserSubscription()).pipe(
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
storagePercentage$: Observable<number> = this.sub$.pipe(
|
||||
map((sub) =>
|
||||
sub != null && sub.maxStorageGb ? +(100 * (sub.storageGb / sub.maxStorageGb)).toFixed(2) : 0,
|
||||
),
|
||||
);
|
||||
|
||||
getFileType(file: FileView): "image" | "pdf" | "text" | "unknown" {
|
||||
switch (file.attachment.fileName.split(".").at(-1)) {
|
||||
case "gif":
|
||||
case "jpeg":
|
||||
case "jpg":
|
||||
return "image";
|
||||
case "pdf":
|
||||
return "pdf";
|
||||
case "txt":
|
||||
return "text";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
async toBlob(file: FileView): Promise<Blob> {
|
||||
const { cipher, attachment } = file;
|
||||
|
||||
let url: string;
|
||||
try {
|
||||
const attachmentDownloadResponse = await this.apiService.getAttachmentData(
|
||||
cipher.id,
|
||||
attachment.id,
|
||||
// this.emergencyAccessId,
|
||||
);
|
||||
url = attachmentDownloadResponse.url;
|
||||
} catch (e) {
|
||||
if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) {
|
||||
url = attachment.url;
|
||||
} else if (e instanceof ErrorResponse) {
|
||||
throw new Error((e as ErrorResponse).getSingleMessage());
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(new Request(url, { cache: "no-store" }));
|
||||
if (response.status !== 200) {
|
||||
throw new Error("blob download error");
|
||||
}
|
||||
|
||||
const encBuf = await EncArrayBuffer.fromResponse(response);
|
||||
const key =
|
||||
attachment.key != null
|
||||
? attachment.key
|
||||
: await this.keyService.getOrgKey(cipher.organizationId);
|
||||
const decBuf = await this.encryptService.decryptToBytes(encBuf, key);
|
||||
return new Blob([decBuf]);
|
||||
}
|
||||
}
|
||||
9
apps/web/src/app/vault/files/files-layout.component.html
Normal file
9
apps/web/src/app/vault/files/files-layout.component.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<app-layout>
|
||||
<app-side-nav>
|
||||
<bit-nav-logo [openIcon]="$any({})" route="." label="File Manager"></bit-nav-logo>
|
||||
<bit-nav-item icon="bwi-vault" text="Vaults"></bit-nav-item>
|
||||
<bit-nav-item icon="bwi-send" text="Send"></bit-nav-item>
|
||||
<bit-nav-item icon="bwi-cog" text="Settings"></bit-nav-item>
|
||||
</app-side-nav>
|
||||
<router-outlet></router-outlet>
|
||||
</app-layout>
|
||||
13
apps/web/src/app/vault/files/files-layout.component.ts
Normal file
13
apps/web/src/app/vault/files/files-layout.component.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
|
||||
import { HeaderModule } from "../../layouts/header/header.module";
|
||||
import { WebLayoutModule } from "../../layouts/web-layout.module";
|
||||
import { SharedModule } from "../../shared";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
templateUrl: "files-layout.component.html",
|
||||
imports: [SharedModule, HeaderModule, WebLayoutModule, RouterModule],
|
||||
})
|
||||
export class FilesLayoutComponent {}
|
||||
41
apps/web/src/app/vault/files/files.component.html
Normal file
41
apps/web/src/app/vault/files/files.component.html
Normal file
@@ -0,0 +1,41 @@
|
||||
<app-header title="File vault">
|
||||
<bit-search placeholder="Search files" class="tw-w-80"></bit-search>
|
||||
<button type="button" bitButton buttonType="primary">
|
||||
<i class="bwi bwi-plus-f tw-mr-2" aria-hidden="true"></i>
|
||||
{{ "Upload" }}
|
||||
</button>
|
||||
</app-header>
|
||||
|
||||
<bit-section class="tw-block tw-p-4">
|
||||
<bit-table-scroll [dataSource]="tableDataSource" [rowSize]="47">
|
||||
<ng-container header>
|
||||
<th></th>
|
||||
<!-- FIXME: sort -->
|
||||
<th bitCell bitSortable="attachment.fileName">Name</th>
|
||||
<th bitCell bitSortable="attachment.sizeName">Size</th>
|
||||
</ng-container>
|
||||
<ng-template bitRowDef let-row>
|
||||
<td bitCell>
|
||||
<i class="bwi bwi-file"></i>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<button type="button" bitLink (click)="openFilePreview(row)">
|
||||
{{ row.attachment.fileName }}
|
||||
</button>
|
||||
</td>
|
||||
<td bitCell>{{ row.attachment.sizeName }}</td>
|
||||
</ng-template>
|
||||
</bit-table-scroll>
|
||||
|
||||
<div *ngIf="fileService.sub$ | async as sub" class="tw-flex tw-gap-4 tw-min-w-full tw-mt-4">
|
||||
<span bitTypography="helper" class="tw-min-w-max">{{
|
||||
sub.storageName + " / " + sub.maxStorageGb + " GB"
|
||||
}}</span>
|
||||
<bit-progress
|
||||
class="tw-w-full"
|
||||
[barWidth]="fileService.storagePercentage$ | async"
|
||||
bgColor="success"
|
||||
size="default"
|
||||
></bit-progress>
|
||||
</div>
|
||||
</bit-section>
|
||||
47
apps/web/src/app/vault/files/files.component.ts
Normal file
47
apps/web/src/app/vault/files/files.component.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Component, OnInit, inject } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
|
||||
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
DialogService,
|
||||
NoItemsModule,
|
||||
SearchModule,
|
||||
TableDataSource,
|
||||
TableModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { HeaderModule } from "../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../shared";
|
||||
|
||||
import { FilePreviewDialogComponent } from "./file-preview-dialog.component";
|
||||
import { FileService } from "./file.service";
|
||||
|
||||
export type FileView = {
|
||||
cipher: CipherView;
|
||||
attachment: AttachmentView;
|
||||
};
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
templateUrl: "files.component.html",
|
||||
imports: [SharedModule, HeaderModule, TableModule, SearchModule, NoItemsModule],
|
||||
})
|
||||
export class FilesComponent implements OnInit {
|
||||
protected fileService = inject(FileService);
|
||||
private dialogService = inject(DialogService);
|
||||
|
||||
protected tableDataSource = new TableDataSource();
|
||||
|
||||
constructor() {
|
||||
this.fileService.files$.pipe(takeUntilDestroyed()).subscribe((files) => {
|
||||
this.tableDataSource.data = files;
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnInit() {}
|
||||
|
||||
protected openFilePreview(file: FileView) {
|
||||
FilePreviewDialogComponent.open(this.dialogService, file);
|
||||
}
|
||||
}
|
||||
@@ -265,6 +265,8 @@ const devServer =
|
||||
https://www.paypalobjects.com
|
||||
https://q.stripe.com
|
||||
https://haveibeenpwned.com
|
||||
https://localhost:8080
|
||||
blob:
|
||||
;child-src
|
||||
'self'
|
||||
https://js.stripe.com
|
||||
|
||||
@@ -158,6 +158,7 @@ export class AttachmentsComponent implements OnInit {
|
||||
this.emergencyAccessId,
|
||||
);
|
||||
url = attachmentDownloadResponse.url;
|
||||
alert(url);
|
||||
} catch (e) {
|
||||
if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) {
|
||||
url = attachment.url;
|
||||
@@ -239,6 +240,7 @@ export class AttachmentsComponent implements OnInit {
|
||||
this.reuploadPromises[attachment.id] = Promise.resolve().then(async () => {
|
||||
// 1. Download
|
||||
a.downloading = true;
|
||||
alert(attachment.url);
|
||||
const response = await fetch(new Request(attachment.url, { cache: "no-store" }));
|
||||
if (response.status !== 200) {
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred"));
|
||||
|
||||
Reference in New Issue
Block a user