UI: Sys Admin home page

This commit is contained in:
Igor Kulikov 2023-04-05 16:31:39 +03:00
parent f10113f20c
commit 92b5b2ee85
15 changed files with 607 additions and 49 deletions

View File

@ -26,6 +26,7 @@ import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.FeaturesInfo;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.SystemInfo;
import org.thingsboard.server.common.data.SystemInfoData;
import org.thingsboard.server.common.data.id.TenantId;
@ -145,9 +146,7 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
AdminSettings mailSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail");
if (mailSettings != null) {
JsonNode mailFrom = mailSettings.getJsonValue().get("mailFrom");
if (mailFrom != null) {
return DataValidator.doValidateEmail(mailFrom.asText());
}
return StringUtils.isNotEmpty(mailFrom.asText());
}
return false;
}

View File

@ -78,7 +78,9 @@ public class DefaultUpdateService implements UpdateService {
@PostConstruct
private void init() {
version = buildProperties != null ? buildProperties.getVersion() : "unknown";
updateMessage = new UpdateMessage(false, version, "", "");
updateMessage = new UpdateMessage(false, version, "", "",
"https://thingsboard.io/docs/reference/releases",
"https://thingsboard.io/docs/reference/releases");
if (updatesEnabled) {
try {
platform = System.getProperty("platform", "unknown");
@ -135,7 +137,9 @@ public class DefaultUpdateService implements UpdateService {
response.get("updateAvailable").asBoolean(),
version,
"3.5.0",
"https://thingsboard.io/docs/user-guide/install/pe/upgrade-instructions"
"https://thingsboard.io/docs/user-guide/install/upgrade-instructions",
"https://thingsboard.io/docs/reference/releases",
"https://thingsboard.io/docs/reference/releases"
);
if (updateMessage.isUpdateAvailable() && !updateMessage.equals(prevUpdateMessage)) {
notificationRuleProcessingService.process(TenantId.SYS_TENANT_ID, NewPlatformVersionTrigger.builder()

View File

@ -24,13 +24,16 @@ import lombok.Data;
public class UpdateMessage {
@ApiModelProperty(position = 1, value = "'True' if new platform update is available.")
private final boolean isUpdateAvailable;
private final boolean updateAvailable;
@ApiModelProperty(position = 2, value = "Current ThingsBoard version.")
private final String currentVersion;
@ApiModelProperty(position = 3, value = "Latest ThingsBoard version.")
private final String latestVersion;
@ApiModelProperty(position = 4, value = "Upgrade instructions URL.")
private final String upgradeInstructionsUrl;
@ApiModelProperty(position = 5, value = "Current ThingsBoard version release notes URL.")
private final String currentVersionReleaseNotesUrl;
@ApiModelProperty(position = 6, value = "Latest ThingsBoard version release notes URL.")
private final String latestVersionReleaseNotesUrl;
}

View File

@ -297,16 +297,6 @@ export class AuthService {
} else if (authState.authUser.isPublic) {
result = this.router.parseUrl(`dashboard/${authState.lastPublicDashboardId}`);
}
} else if (authState.authUser.authority === Authority.SYS_ADMIN) {
this.adminService.checkUpdates().subscribe((updateMessage) => {
if (updateMessage && updateMessage.updateAvailable) {
this.store.dispatch(new ActionNotificationShow(
{message: updateMessage.message,
type: 'info',
verticalPosition: 'bottom',
horizontalPosition: 'right'}));
}
});
}
}
} else {

View File

@ -21,6 +21,7 @@ import { HttpClient } from '@angular/common/http';
import {
AdminSettings,
AutoCommitSettings,
FeaturesInfo,
JwtSettings,
MailServerSettings,
RepositorySettings,
@ -131,4 +132,8 @@ export class AdminService {
public checkUpdates(config?: RequestConfig): Observable<UpdateMessage> {
return this.http.get<UpdateMessage>(`/api/admin/updates`, defaultHttpOptionsFromConfig(config));
}
public getFeaturesInfo(config?: RequestConfig): Observable<FeaturesInfo> {
return this.http.get<FeaturesInfo>('/api/admin/featuresInfo', defaultHttpOptionsFromConfig(config));
}
}

View File

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { AfterViewInit, ChangeDetectionStrategy, Component, NgZone, OnInit, ViewChild } from '@angular/core';
import { AfterViewInit, Component, NgZone, OnInit, ViewChild } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -44,8 +44,7 @@ export interface SystemInfoData {
@Component({
selector: 'tb-cluster-info-table',
templateUrl: './cluster-info-table.component.html',
styleUrls: ['./cluster-info-table.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
styleUrls: ['./cluster-info-table.component.scss']
})
export class ClusterInfoTableComponent extends PageComponent implements OnInit, AfterViewInit {

View File

@ -0,0 +1,66 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-card-content" fxLayout="column" fxLayoutGap="8px">
<div class="tb-title">Configured features
<mat-icon
matTooltip="Status of features that require configuration"
matTooltipPosition="above"
class="tb-info-icon">info</mat-icon>
</div>
<div fxLayout="column" fxFlex fxLayoutGap="12px">
<div fxLayout="row" fxFlex fxLayoutGap="12px">
<a class="tb-feature-button" fxFlex
[matTooltip]="featureTooltip(featuresInfo?.emailEnabled)"
matTooltipPosition="above"
[ngClass]="{'configured': featuresInfo?.emailEnabled}"
routerLink="/settings/outgoing-mail">
<div class="tb-feature-text">Email</div>
</a>
<a class="tb-feature-button" fxFlex
[matTooltip]="featureTooltip(featuresInfo?.smsEnabled)"
matTooltipPosition="above"
[ngClass]="{'configured': featuresInfo?.smsEnabled}"
routerLink="/settings/notifications">
<div class="tb-feature-text">SMS</div>
</a>
</div>
<div fxLayout="row" fxFlex fxLayoutGap="12px">
<a class="tb-feature-button" fxFlex
[matTooltip]="featureTooltip(featuresInfo?.notificationEnabled)"
matTooltipPosition="above"
[ngClass]="{'configured': featuresInfo?.notificationEnabled}"
routerLink="/settings/notifications">
<div class="tb-feature-text">Slack</div>
</a>
<a class="tb-feature-button" fxFlex
[matTooltip]="featureTooltip(featuresInfo?.oauthEnabled)"
matTooltipPosition="above"
[ngClass]="{'configured': featuresInfo?.oauthEnabled}"
routerLink="/security-settings/oauth2">
<div class="tb-feature-text">OAuth 2</div>
</a>
<a class="tb-feature-button" fxFlex
[matTooltip]="featureTooltip(featuresInfo?.twoFaEnabled)"
matTooltipPosition="above"
[ngClass]="{'configured': featuresInfo?.twoFaEnabled}"
routerLink="/security-settings/2fa">
<div class="tb-feature-text">2FA</div>
</a>
</div>
</div>
</div>

View File

@ -0,0 +1,85 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.tb-card-content {
width: 100%;
height: 100%;
}
.tb-title {
font-style: normal;
font-weight: 500;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.54);
}
.tb-info-icon {
color: rgba(0, 0, 0, 0.12);
font-size: 16px;
width: 16px;
height: 16px;
line-height: 15px;
vertical-align: middle;
}
.tb-feature-button {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
padding: 12px;
background: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.05);
box-shadow: 0 5px 16px rgba(0, 0, 0, 0.04);
border-radius: 10px;
&:hover {
border: 1px solid rgba(0, 0, 0, 0.12);
box-shadow: 0 4px 10px rgba(23, 33, 90, 0.08);
}
.tb-feature-text {
font-weight: 400;
font-size: 14px;
line-height: 20px;
color: rgba(0, 0, 0, 0.87);
&:before {
content: "close";
font-family: 'Material Icons Round';
font-size: 20px;
line-height: 1;
color: rgba(0, 0, 0, 0.12);
font-weight: 600;
position: relative;
background: #F4F4F4;
border-radius: 4px;
width: 24px;
height: 24px;
margin-right: 8px;
vertical-align: bottom;
}
}
&.configured {
.tb-feature-text {
&:before {
content: "check";
color: #198038;
background: #F3F6FA;
}
}
}
}
}

View File

@ -0,0 +1,60 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { AdminService } from '@core/http/admin.service';
import { FeaturesInfo } from '@shared/models/settings.models';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { Authority } from '@shared/models/authority.enum';
import { of } from 'rxjs';
@Component({
selector: 'tb-configured-features',
templateUrl: './configured-features.component.html',
styleUrls: ['./configured-features.component.scss']
})
export class ConfiguredFeaturesComponent extends PageComponent implements OnInit {
authUser = getCurrentAuthUser(this.store);
featuresInfo: FeaturesInfo;
constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef,
private adminService: AdminService) {
super(store);
}
ngOnInit() {
(this.authUser.authority === Authority.SYS_ADMIN ?
this.adminService.getFeaturesInfo() : of(null)).subscribe(
(featuresInfo) => {
this.featuresInfo = featuresInfo;
this.cd.markForCheck();
}
);
}
featureTooltip(configured: boolean): string {
if (configured) {
return 'Feature is configured.\nClick to setup';
} else {
return 'Feature is not configured.\nClick to setup';
}
}
}

View File

@ -18,18 +18,24 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@app/shared/shared.module';
import { ClusterInfoTableComponent } from '@home/components/widget/lib/home-page/cluster-info-table.component';
import { ConfiguredFeaturesComponent } from '@home/components/widget/lib/home-page/configured-features.component';
import { VersionInfoComponent } from '@home/components/widget/lib/home-page/version-info.component';
@NgModule({
declarations:
[
ClusterInfoTableComponent
ClusterInfoTableComponent,
ConfiguredFeaturesComponent,
VersionInfoComponent
],
imports: [
CommonModule,
SharedModule
],
exports: [
ClusterInfoTableComponent
ClusterInfoTableComponent,
ConfiguredFeaturesComponent,
VersionInfoComponent
]
})
export class HomePageWidgetsModule { }

View File

@ -0,0 +1,44 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-card-content" fxLayout="column" fxLayoutGap="8px">
<div class="tb-card-header">
<div class="tb-title">Version</div>
<a mat-button color="primary" href="https://thingsboard.io/docs/contact-us/" target="_blank">Contact us</a>
</div>
<div fxLayout="column" fxFlex fxLayoutGap="12px">
<div class="tb-version-info-container" fxFlex>
<div class="tb-card-header">
<a class="tb-version-link" [href]="updateMessage?.currentVersionReleaseNotesUrl" target="_blank">Current version</a>
</div>
<div class="tb-version">{{ updateMessage?.currentVersion }}</div>
</div>
<div class="tb-version-info-container" fxFlex *ngIf="updateMessage?.updateAvailable; else versionUpToDate">
<div class="tb-card-header">
<a class="tb-version-link" [href]="updateMessage?.latestVersionReleaseNotesUrl" target="_blank">Available version</a>
<a mat-flat-button color="primary" [href]="updateMessage?.upgradeInstructionsUrl" target="_blank">Upgrade</a>
</div>
<div class="tb-version">{{ updateMessage?.latestVersion }}</div>
</div>
</div>
</div>
<ng-template #versionUpToDate>
<div class="tb-version-info-container up-to-date" fxFlex>
<div class="material-icons-round check-icon">check</div>
<div class="up-to-date-text">Version is up to date</div>
</div>
</ng-template>

View File

@ -0,0 +1,102 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.tb-card-content {
width: 100%;
height: 100%;
}
.tb-card-header {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.tb-title {
font-style: normal;
font-weight: 500;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.54);
}
.tb-version-info-container {
border: 1px solid rgba(0, 0, 0, 0.05);
box-shadow: 0 5px 16px rgba(0, 0, 0, 0.04);
border-radius: 10px;
padding: 16px;
display: flex;
flex-direction: column;
justify-content: space-evenly;
&.up-to-date {
box-shadow: none;
border: none;
align-items: center;
background: #F3F6FA;
}
}
.tb-version-link {
font-weight: 400;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.76);
position: relative;
border-bottom: none;
&:hover, &:focus {
border-bottom: none;
}
&::after {
content: 'arrow_forward';
display: inline-block;
position: absolute;
top: -2px;
right: -28px;
transform: rotate(315deg);
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 18px;
color: rgba(0, 0, 0, 0.12);
}
&:hover::after {
color: inherit;
}
}
.tb-version {
font-weight: 500;
font-size: 18px;
line-height: 24px;
letter-spacing: 0.15px;
color: rgba(0, 0, 0, 0.87);
}
.check-icon {
color: #198038;
font-weight: 600;
}
.up-to-date-text {
font-weight: 400;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.76);
}
}

View File

@ -0,0 +1,60 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { AdminService } from '@core/http/admin.service';
import { UpdateMessage } from '@shared/models/settings.models';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { Authority } from '@shared/models/authority.enum';
import { of } from 'rxjs';
@Component({
selector: 'tb-version-info',
templateUrl: './version-info.component.html',
styleUrls: ['./version-info.component.scss']
})
export class VersionInfoComponent extends PageComponent implements OnInit {
authUser = getCurrentAuthUser(this.store);
updateMessage: UpdateMessage;
constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef,
private adminService: AdminService) {
super(store);
}
ngOnInit() {
(this.authUser.authority === Authority.SYS_ADMIN ?
this.adminService.checkUpdates() : of(null)).subscribe(
(updateMessage) => {
this.updateMessage = updateMessage;
this.cd.markForCheck();
}
);
}
featureTooltip(configured: boolean): string {
if (configured) {
return 'Feature is configured.\nClick to setup';
} else {
return 'Feature is not configured.\nClick to setup';
}
}
}

View File

@ -1561,30 +1561,7 @@
"sizeX": 5,
"sizeY": 3.5,
"config": {
"datasources": [
{
"type": "entityCount",
"name": null,
"entityAliasId": "ae870700-071f-b3bc-406c-16ba554c5a55",
"filterId": null,
"dataKeys": [
{
"name": "count",
"type": "count",
"label": "tenantProfilesCount",
"color": "#2196f3",
"settings": {},
"_hash": 0.8491768696709192,
"aggregationType": null,
"units": null,
"decimals": null,
"funcBody": null,
"usePostProcessing": null,
"postFuncBody": null
}
]
}
],
"datasources": [],
"timewindow": {
"displayValue": "",
"selectedTab": 0,
@ -1640,6 +1617,140 @@
"row": 0,
"col": 0,
"id": "8acbf5df-f9fc-114d-216f-86f081aa4779"
},
"d2784f6c-0518-fd95-1d28-b21f70bdcb10": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5,
"sizeY": 3.5,
"config": {
"datasources": [],
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1680168340431,
"endTimeMs": 1680254740431
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
}
},
"showTitle": false,
"backgroundColor": "#fff",
"color": "rgba(0, 0, 0, 0.87)",
"padding": "16px",
"settings": {
"useMarkdownTextFunction": false,
"markdownTextPattern": "<div style=\"height: 100%;\">\n <tb-configured-features></tb-configured-features>\n</div>",
"applyDefaultMarkdownStyle": false
},
"title": "Configured features",
"showTitleIcon": false,
"iconColor": "rgba(0, 0, 0, 0.87)",
"iconSize": "24px",
"titleTooltip": "",
"dropShadow": false,
"enableFullscreen": false,
"widgetStyle": {},
"titleStyle": {
"fontSize": "16px",
"fontWeight": 400
},
"showLegend": false,
"useDashboardTimewindow": true,
"widgetCss": "",
"pageSize": 1024,
"noDataDisplayMessage": ""
},
"row": 0,
"col": 0,
"id": "d2784f6c-0518-fd95-1d28-b21f70bdcb10"
},
"66dcf1a2-9d83-6873-c693-d6a5d989b3f8": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5,
"sizeY": 3.5,
"config": {
"datasources": [],
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1680168340431,
"endTimeMs": 1680254740431
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
}
},
"showTitle": false,
"backgroundColor": "#fff",
"color": "rgba(0, 0, 0, 0.87)",
"padding": "16px",
"settings": {
"useMarkdownTextFunction": false,
"markdownTextPattern": "<div style=\"height: 100%;\">\n <tb-version-info></tb-version-info>\n</div>",
"applyDefaultMarkdownStyle": false
},
"title": "Version",
"showTitleIcon": false,
"iconColor": "rgba(0, 0, 0, 0.87)",
"iconSize": "24px",
"titleTooltip": "",
"dropShadow": false,
"enableFullscreen": false,
"widgetStyle": {},
"titleStyle": {
"fontSize": "16px",
"fontWeight": 400
},
"showLegend": false,
"useDashboardTimewindow": true,
"widgetCss": "",
"pageSize": 1024,
"noDataDisplayMessage": ""
},
"row": 0,
"col": 0,
"id": "66dcf1a2-9d83-6873-c693-d6a5d989b3f8"
}
},
"states": {
@ -1708,6 +1819,18 @@
"sizeY": 7,
"row": 19,
"col": 11
},
"d2784f6c-0518-fd95-1d28-b21f70bdcb10": {
"sizeX": 20,
"sizeY": 7,
"row": 12,
"col": 0
},
"66dcf1a2-9d83-6873-c693-d6a5d989b3f8": {
"sizeX": 11,
"sizeY": 7,
"row": 19,
"col": 0
}
},
"gridSettings": {
@ -1810,7 +1933,7 @@
"filter": {
"type": "entityType",
"resolveMultiple": true,
"entityType": "TENANT"
"entityType": "TENANT_PROFILE"
}
},
"a1ddb8fa-90ff-5598-e7f2-e254194d055d": {
@ -1908,4 +2031,4 @@
},
"externalId": null,
"name": "System Administrator Home Page"
}
}

View File

@ -71,8 +71,12 @@ export interface JwtSettings {
}
export interface UpdateMessage {
message: string;
updateAvailable: boolean;
currentVersion: string;
latestVersion: string;
upgradeInstructionsUrl: string;
currentVersionReleaseNotesUrl: string;
latestVersionReleaseNotesUrl: string;
}
export const phoneNumberPattern = /^\+[1-9]\d{1,14}$/;
@ -437,3 +441,11 @@ export interface AutoVersionCreateConfig extends VersionCreateConfig {
}
export type AutoCommitSettings = {[entityType: string]: AutoVersionCreateConfig};
export interface FeaturesInfo {
emailEnabled: boolean;
smsEnabled: boolean;
notificationEnabled: boolean;
oauthEnabled: boolean;
twoFaEnabled: boolean;
}