[PM-11901] Refactoring self-hosting license file uploader (#11083)
This commit is contained in:
parent
8fb97e7b60
commit
d2e5af7fb5
|
@ -66,33 +66,39 @@
|
|||
</bit-callout>
|
||||
</bit-section>
|
||||
<bit-section *ngIf="isSelfHost">
|
||||
<p bitTypography="body1">{{ "uploadLicenseFilePremium" | i18n }}</p>
|
||||
<form [formGroup]="licenseFormGroup" [bitSubmit]="submitPremiumLicense">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
|
||||
<div>
|
||||
<button type="button" bitButton buttonType="secondary" (click)="fileSelector.click()">
|
||||
{{ "chooseFile" | i18n }}
|
||||
</button>
|
||||
{{
|
||||
licenseFormGroup.value.file ? licenseFormGroup.value.file.name : ("noFileChosen" | i18n)
|
||||
}}
|
||||
</div>
|
||||
<input
|
||||
bitInput
|
||||
#fileSelector
|
||||
type="file"
|
||||
formControlName="file"
|
||||
(change)="onLicenseFileSelected($event)"
|
||||
hidden
|
||||
class="tw-hidden"
|
||||
/>
|
||||
<bit-hint>{{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<button type="submit" buttonType="primary" bitButton bitFormButton>
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
<ng-container *ngIf="!(useLicenseUploaderComponent$ | async)">
|
||||
<p bitTypography="body1">{{ "uploadLicenseFilePremium" | i18n }}</p>
|
||||
<form [formGroup]="licenseFormGroup" [bitSubmit]="submitPremiumLicense">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
|
||||
<div>
|
||||
<button type="button" bitButton buttonType="secondary" (click)="fileSelector.click()">
|
||||
{{ "chooseFile" | i18n }}
|
||||
</button>
|
||||
{{
|
||||
licenseFormGroup.value.file ? licenseFormGroup.value.file.name : ("noFileChosen" | i18n)
|
||||
}}
|
||||
</div>
|
||||
<input
|
||||
bitInput
|
||||
#fileSelector
|
||||
type="file"
|
||||
formControlName="file"
|
||||
(change)="onLicenseFileSelected($event)"
|
||||
hidden
|
||||
class="tw-hidden"
|
||||
/>
|
||||
<bit-hint>{{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<button type="submit" buttonType="primary" bitButton bitFormButton>
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
</ng-container>
|
||||
<individual-self-hosting-license-uploader
|
||||
*ngIf="useLicenseUploaderComponent$ | async"
|
||||
(onLicenseFileUploaded)="onLicenseFileSelectedChanged()"
|
||||
/>
|
||||
</bit-section>
|
||||
<form *ngIf="!isSelfHost" [formGroup]="addOnFormGroup" [bitSubmit]="submitPayment">
|
||||
<bit-section>
|
||||
|
|
|
@ -7,6 +7,8 @@ import { combineLatest, concatMap, from, Observable, of } from "rxjs";
|
|||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
@ -36,6 +38,10 @@ export class PremiumV2Component {
|
|||
protected cloudWebVaultURL: string;
|
||||
protected isSelfHost = false;
|
||||
|
||||
protected useLicenseUploaderComponent$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader,
|
||||
);
|
||||
|
||||
protected readonly familyPlanMaxUserCount = 6;
|
||||
protected readonly premiumPrice = 10;
|
||||
protected readonly storageGBPrice = 4;
|
||||
|
@ -44,6 +50,7 @@ export class PremiumV2Component {
|
|||
private activatedRoute: ActivatedRoute,
|
||||
private apiService: ApiService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private configService: ConfigService,
|
||||
private environmentService: EnvironmentService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
|
@ -78,6 +85,9 @@ export class PremiumV2Component {
|
|||
finalizeUpgrade = async () => {
|
||||
await this.apiService.refreshIdentityToken();
|
||||
await this.syncService.fullSync(true);
|
||||
};
|
||||
|
||||
postFinalizeUpgrade = async () => {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
|
@ -119,6 +129,7 @@ export class PremiumV2Component {
|
|||
|
||||
await this.apiService.postAccountLicense(formData);
|
||||
await this.finalizeUpgrade();
|
||||
await this.postFinalizeUpgrade();
|
||||
};
|
||||
|
||||
submitPayment = async (): Promise<void> => {
|
||||
|
@ -138,6 +149,7 @@ export class PremiumV2Component {
|
|||
|
||||
await this.apiService.postPremium(formData);
|
||||
await this.finalizeUpgrade();
|
||||
await this.postFinalizeUpgrade();
|
||||
};
|
||||
|
||||
protected get additionalStorageCost(): number {
|
||||
|
@ -161,4 +173,8 @@ export class PremiumV2Component {
|
|||
protected get total(): number {
|
||||
return this.subtotal + this.estimatedTax;
|
||||
}
|
||||
|
||||
protected async onLicenseFileSelectedChanged(): Promise<void> {
|
||||
await this.postFinalizeUpgrade();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,32 +7,38 @@
|
|||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="createOrganization && selfHosted">
|
||||
<p bitTypography="body1">{{ "uploadLicenseFileOrg" | i18n }}</p>
|
||||
<form [formGroup]="selfHostedForm" [bitSubmit]="submit">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
|
||||
<div class="tw-pt-2 tw-pb-1">
|
||||
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
|
||||
{{ "chooseFile" | i18n }}
|
||||
</button>
|
||||
{{ selectedFile?.name ?? ("noFileChosen" | i18n) }}
|
||||
</div>
|
||||
<input
|
||||
#fileSelector
|
||||
bitInput
|
||||
type="file"
|
||||
formControlName="file"
|
||||
(change)="setSelectedFile($event)"
|
||||
accept="application/JSON"
|
||||
hidden
|
||||
class="tw-hidden"
|
||||
/>
|
||||
<bit-hint>{{ "licenseFileDesc" | i18n: "bitwarden_organization_license.json" }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
<ng-container *ngIf="!(useLicenseUploaderComponent$ | async)">
|
||||
<p bitTypography="body1">{{ "uploadLicenseFileOrg" | i18n }}</p>
|
||||
<form [formGroup]="selfHostedForm" [bitSubmit]="submit">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
|
||||
<div class="tw-pt-2 tw-pb-1">
|
||||
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
|
||||
{{ "chooseFile" | i18n }}
|
||||
</button>
|
||||
{{ selectedFile?.name ?? ("noFileChosen" | i18n) }}
|
||||
</div>
|
||||
<input
|
||||
#fileSelector
|
||||
bitInput
|
||||
type="file"
|
||||
formControlName="file"
|
||||
(change)="setSelectedFile($event)"
|
||||
accept="application/JSON"
|
||||
hidden
|
||||
class="tw-hidden"
|
||||
/>
|
||||
<bit-hint>{{ "licenseFileDesc" | i18n: "bitwarden_organization_license.json" }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
</ng-container>
|
||||
<organization-self-hosting-license-uploader
|
||||
*ngIf="useLicenseUploaderComponent$ | async"
|
||||
(onLicenseFileUploaded)="onLicenseFileUploaded($event)"
|
||||
/>
|
||||
</ng-container>
|
||||
<form
|
||||
[formGroup]="formGroup"
|
||||
|
|
|
@ -117,6 +117,10 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
|||
discount = 0;
|
||||
deprecateStripeSourcesAPI: boolean;
|
||||
|
||||
protected useLicenseUploaderComponent$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader,
|
||||
);
|
||||
|
||||
secretsManagerSubscription = secretsManagerSubscribeFormFactory(this.formBuilder);
|
||||
|
||||
selfHostedForm = this.formBuilder.group({
|
||||
|
@ -855,4 +859,30 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
|||
private planIsEnabled(plan: PlanResponse) {
|
||||
return !plan.disabled && !plan.legacyYear;
|
||||
}
|
||||
|
||||
protected async onLicenseFileUploaded(organizationId: string): Promise<void> {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: this.i18nService.t("organizationCreated"),
|
||||
message: this.i18nService.t("organizationReadyToGo"),
|
||||
});
|
||||
|
||||
if (!this.acceptingSponsorship && !this.isInTrialFlow) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/organizations/" + organizationId]);
|
||||
}
|
||||
|
||||
if (this.isInTrialFlow) {
|
||||
this.onTrialBillingSuccess.emit({
|
||||
orgId: organizationId,
|
||||
subLabelText: this.billingSubLabelText(),
|
||||
});
|
||||
}
|
||||
|
||||
this.onSuccess.emit({ organizationId: organizationId });
|
||||
|
||||
// TODO: No one actually listening to this message?
|
||||
this.messagingService.send("organizationCreated", { organizationId: organizationId });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@ import { OffboardingSurveyComponent } from "./offboarding-survey.component";
|
|||
import { PaymentV2Component } from "./payment/payment-v2.component";
|
||||
import { PaymentComponent } from "./payment/payment.component";
|
||||
import { PaymentMethodComponent } from "./payment-method.component";
|
||||
import { IndividualSelfHostingLicenseUploaderComponent } from "./self-hosting-license-uploader/individual-self-hosting-license-uploader.component";
|
||||
import { OrganizationSelfHostingLicenseUploaderComponent } from "./self-hosting-license-uploader/organization-self-hosting-license-uploader.component";
|
||||
import { SecretsManagerSubscribeComponent } from "./sm-subscribe.component";
|
||||
import { TaxInfoComponent } from "./tax-info.component";
|
||||
import { UpdateLicenseDialogComponent } from "./update-license-dialog.component";
|
||||
|
@ -40,6 +42,8 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac
|
|||
OffboardingSurveyComponent,
|
||||
AdjustPaymentDialogV2Component,
|
||||
AdjustStorageDialogV2Component,
|
||||
IndividualSelfHostingLicenseUploaderComponent,
|
||||
OrganizationSelfHostingLicenseUploaderComponent,
|
||||
],
|
||||
exports: [
|
||||
SharedModule,
|
||||
|
@ -53,6 +57,8 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac
|
|||
OffboardingSurveyComponent,
|
||||
VerifyBankAccountComponent,
|
||||
PaymentV2Component,
|
||||
IndividualSelfHostingLicenseUploaderComponent,
|
||||
OrganizationSelfHostingLicenseUploaderComponent,
|
||||
],
|
||||
})
|
||||
export class BillingSharedModule {}
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
|
||||
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { LicenseUploaderFormModel } from "./license-uploader-form-model";
|
||||
|
||||
/**
|
||||
* Shared implementation for processing license file uploads.
|
||||
* @remarks Requires self-hosting.
|
||||
*/
|
||||
export abstract class AbstractSelfHostingLicenseUploaderComponent {
|
||||
protected form: FormGroup;
|
||||
|
||||
protected constructor(
|
||||
protected readonly formBuilder: FormBuilder,
|
||||
protected readonly i18nService: I18nService,
|
||||
protected readonly platformUtilsService: PlatformUtilsService,
|
||||
protected readonly toastService: ToastService,
|
||||
protected readonly tokenService: TokenService,
|
||||
) {
|
||||
const isSelfHosted = this.platformUtilsService.isSelfHost();
|
||||
|
||||
if (!isSelfHosted) {
|
||||
throw new Error("This component should only be used in self-hosted environments");
|
||||
}
|
||||
|
||||
this.form = this.formBuilder.group({
|
||||
file: [null, [Validators.required]],
|
||||
});
|
||||
this.submit = this.submit.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the submitted license upload form model.
|
||||
* @protected
|
||||
*/
|
||||
protected get formValue(): LicenseUploaderFormModel {
|
||||
return this.form.value as LicenseUploaderFormModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when a different license file is selected.
|
||||
* @param event
|
||||
*/
|
||||
onLicenseFileSelectedChanged(event: Event): void {
|
||||
const element = event.target as HTMLInputElement;
|
||||
this.form.value.file = element.files.length > 0 ? element.files[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the license upload form.
|
||||
* @protected
|
||||
*/
|
||||
protected async submit(): Promise<void> {
|
||||
this.form.markAllAsTouched();
|
||||
|
||||
if (this.form.invalid) {
|
||||
return this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("selectFile"),
|
||||
});
|
||||
}
|
||||
|
||||
const emailVerified = await this.tokenService.getEmailVerified();
|
||||
if (!emailVerified) {
|
||||
return this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("verifyEmailFirst"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
abstract get description(): string;
|
||||
|
||||
abstract get hintFileName(): string;
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
import { Component, EventEmitter, Output } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { AbstractSelfHostingLicenseUploaderComponent } from "../../shared/self-hosting-license-uploader/abstract-self-hosting-license-uploader.component";
|
||||
|
||||
/**
|
||||
* Processes license file uploads for individual plans.
|
||||
* @remarks Requires self-hosting.
|
||||
*/
|
||||
@Component({
|
||||
selector: "individual-self-hosting-license-uploader",
|
||||
templateUrl: "./self-hosting-license-uploader.component.html",
|
||||
})
|
||||
export class IndividualSelfHostingLicenseUploaderComponent extends AbstractSelfHostingLicenseUploaderComponent {
|
||||
/**
|
||||
* Emitted when a license file has been successfully uploaded & processed.
|
||||
*/
|
||||
@Output() onLicenseFileUploaded: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
constructor(
|
||||
protected readonly apiService: ApiService,
|
||||
protected readonly formBuilder: FormBuilder,
|
||||
protected readonly i18nService: I18nService,
|
||||
protected readonly platformUtilsService: PlatformUtilsService,
|
||||
protected readonly syncService: SyncService,
|
||||
protected readonly toastService: ToastService,
|
||||
protected readonly tokenService: TokenService,
|
||||
) {
|
||||
super(formBuilder, i18nService, platformUtilsService, toastService, tokenService);
|
||||
}
|
||||
|
||||
protected async submit(): Promise<void> {
|
||||
await super.submit();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("license", this.formValue.file);
|
||||
|
||||
await this.apiService.postAccountLicense(formData);
|
||||
|
||||
await this.apiService.refreshIdentityToken();
|
||||
await this.syncService.fullSync(true);
|
||||
|
||||
this.onLicenseFileUploaded.emit();
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
return "uploadLicenseFilePremium";
|
||||
}
|
||||
|
||||
get hintFileName(): string {
|
||||
return "bitwarden_premium_license.json";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export interface LicenseUploaderFormModel {
|
||||
file: File;
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
import { Component, EventEmitter, Output } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { AbstractSelfHostingLicenseUploaderComponent } from "../../shared/self-hosting-license-uploader/abstract-self-hosting-license-uploader.component";
|
||||
|
||||
/**
|
||||
* Processes license file uploads for organizations.
|
||||
* @remarks Requires self-hosting.
|
||||
*/
|
||||
@Component({
|
||||
selector: "organization-self-hosting-license-uploader",
|
||||
templateUrl: "./self-hosting-license-uploader.component.html",
|
||||
})
|
||||
export class OrganizationSelfHostingLicenseUploaderComponent extends AbstractSelfHostingLicenseUploaderComponent {
|
||||
/**
|
||||
* Notifies the parent component of the `organizationId` the license was created for.
|
||||
*/
|
||||
@Output() onLicenseFileUploaded: EventEmitter<string> = new EventEmitter<string>();
|
||||
|
||||
constructor(
|
||||
protected readonly formBuilder: FormBuilder,
|
||||
protected readonly i18nService: I18nService,
|
||||
protected readonly platformUtilsService: PlatformUtilsService,
|
||||
protected readonly toastService: ToastService,
|
||||
protected readonly tokenService: TokenService,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly encryptService: EncryptService,
|
||||
private readonly cryptoService: CryptoService,
|
||||
private readonly organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private readonly syncService: SyncService,
|
||||
) {
|
||||
super(formBuilder, i18nService, platformUtilsService, toastService, tokenService);
|
||||
}
|
||||
|
||||
protected async submit(): Promise<void> {
|
||||
await super.submit();
|
||||
|
||||
const orgKey = await this.cryptoService.makeOrgKey<OrgKey>();
|
||||
const key = orgKey[0].encryptedString;
|
||||
const collection = await this.encryptService.encrypt(
|
||||
this.i18nService.t("defaultCollection"),
|
||||
orgKey[1],
|
||||
);
|
||||
const collectionCt = collection.encryptedString;
|
||||
const orgKeys = await this.cryptoService.makeKeyPair(orgKey[1]);
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append("license", this.formValue.file);
|
||||
fd.append("key", key);
|
||||
fd.append("collectionName", collectionCt);
|
||||
const response = await this.organizationApiService.createLicense(fd);
|
||||
const orgId = response.id;
|
||||
|
||||
await this.apiService.refreshIdentityToken();
|
||||
|
||||
// Org Keys live outside of the OrganizationLicense - add the keys to the org here
|
||||
const request = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
|
||||
await this.organizationApiService.updateKeys(orgId, request);
|
||||
|
||||
await this.apiService.refreshIdentityToken();
|
||||
await this.syncService.fullSync(true);
|
||||
|
||||
this.onLicenseFileUploaded.emit(orgId);
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
return "uploadLicenseFileOrg";
|
||||
}
|
||||
|
||||
get hintFileName(): string {
|
||||
return "bitwarden_organization_license.json";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
<p bitTypography="body1">{{ "uploadLicenseFileOrg" | i18n }}</p>
|
||||
<form [formGroup]="form" [bitSubmit]="submit">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ description | i18n }}</bit-label>
|
||||
<div>
|
||||
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
|
||||
{{ "chooseFile" | i18n }}
|
||||
</button>
|
||||
{{ form.value.file ? form.value.file.name : ("noFileChosen" | i18n) }}
|
||||
</div>
|
||||
<input
|
||||
#fileSelector
|
||||
bitInput
|
||||
type="file"
|
||||
formControlName="file"
|
||||
(change)="onLicenseFileSelectedChanged($event)"
|
||||
accept="application/JSON"
|
||||
hidden
|
||||
class="tw-hidden"
|
||||
/>
|
||||
<bit-hint>{{ "licenseFileDesc" | i18n: hintFileName }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
</form>
|
|
@ -33,6 +33,7 @@ export enum FeatureFlag {
|
|||
NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements",
|
||||
AC2476_DeprecateStripeSourcesAPI = "AC-2476-deprecate-stripe-sources-api",
|
||||
CipherKeyEncryption = "cipher-key-encryption",
|
||||
PM11901_RefactorSelfHostingLicenseUploader = "PM-11901-refactor-self-hosting-license-uploader",
|
||||
}
|
||||
|
||||
export type AllowedFeatureFlagTypes = boolean | number | string;
|
||||
|
@ -76,6 +77,7 @@ export const DefaultFeatureFlagValue = {
|
|||
[FeatureFlag.NotificationBarAddLoginImprovements]: FALSE,
|
||||
[FeatureFlag.AC2476_DeprecateStripeSourcesAPI]: FALSE,
|
||||
[FeatureFlag.CipherKeyEncryption]: FALSE,
|
||||
[FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader]: FALSE,
|
||||
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
|
||||
|
||||
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
||||
|
|
Loading…
Reference in New Issue