diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs index c3dc47147..aaae69798 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/Queries/FilterVisitor.cs @@ -86,7 +86,7 @@ namespace Squidex.Infrastructure.MongoDb.Queries private static BsonRegularExpression BuildRegex(CompareFilter node, Func formatter) { - return new BsonRegularExpression(formatter(node.Value.Value!.ToString()!), "i"); + return new BsonRegularExpression(formatter(node.Value.Value?.ToString() ?? "null"), "i"); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs index 28520ca99..88e8c9779 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs @@ -183,6 +183,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb Assert.Equal(o, i); } + [Fact] + public void Should_make_query_with_null_regex() + { + var i = _F(ClrFilter.Contains("createdBy", null!)); + var o = _C("{ 'cb' : /null/i }"); + + Assert.Equal(o, i); + } + [Fact] public void Should_make_query_with_empty_test() { diff --git a/frontend/app/features/dashboard/declarations.ts b/frontend/app/features/dashboard/declarations.ts index 744fb2e17..2473b8e88 100644 --- a/frontend/app/features/dashboard/declarations.ts +++ b/frontend/app/features/dashboard/declarations.ts @@ -10,6 +10,7 @@ export * from './pages/cards/api-calls-summary-card.component'; export * from './pages/cards/api-card.component'; export * from './pages/cards/api-performance-card.component'; export * from './pages/cards/api-traffic-card.component'; +export * from './pages/cards/api-traffic-summary-card.component'; export * from './pages/cards/asset-uploads-count-card.component'; export * from './pages/cards/asset-uploads-size-card.component'; export * from './pages/cards/asset-uploads-size-summary-card.component'; diff --git a/frontend/app/features/dashboard/module.ts b/frontend/app/features/dashboard/module.ts index efedf71a3..050f0f2c1 100644 --- a/frontend/app/features/dashboard/module.ts +++ b/frontend/app/features/dashboard/module.ts @@ -12,7 +12,7 @@ import { RouterModule, Routes } from '@angular/router'; import { SqxFrameworkModule, SqxSharedModule } from '@app/shared'; import { GridsterModule } from 'angular-gridster2'; import { ChartModule } from 'angular2-chartjs'; -import { ApiCallsCardComponent, ApiCallsSummaryCardComponent, ApiCardComponent, ApiPerformanceCardComponent, ApiTrafficCardComponent, AssetUploadsCountCardComponent, AssetUploadsSizeCardComponent, AssetUploadsSizeSummaryCardComponent, ContentSummaryCardComponent, DashboardConfigComponent, DashboardPageComponent, GithubCardComponent, HistoryCardComponent, IFrameCardComponent, SchemaCardComponent, SupportCardComponent } from './declarations'; +import { ApiCallsCardComponent, ApiCallsSummaryCardComponent, ApiCardComponent, ApiPerformanceCardComponent, ApiTrafficCardComponent, ApiTrafficSummaryCardComponent, AssetUploadsCountCardComponent, AssetUploadsSizeCardComponent, AssetUploadsSizeSummaryCardComponent, ContentSummaryCardComponent, DashboardConfigComponent, DashboardPageComponent, GithubCardComponent, HistoryCardComponent, IFrameCardComponent, SchemaCardComponent, SupportCardComponent } from './declarations'; const routes: Routes = [ { @@ -35,6 +35,7 @@ const routes: Routes = [ ApiCardComponent, ApiPerformanceCardComponent, ApiTrafficCardComponent, + ApiTrafficSummaryCardComponent, AssetUploadsCountCardComponent, AssetUploadsSizeCardComponent, AssetUploadsSizeSummaryCardComponent, diff --git a/frontend/app/features/dashboard/pages/cards/api-traffic-card.component.html b/frontend/app/features/dashboard/pages/cards/api-traffic-card.component.html index 531c15893..6754982ad 100644 --- a/frontend/app/features/dashboard/pages/cards/api-traffic-card.component.html +++ b/frontend/app/features/dashboard/pages/cards/api-traffic-card.component.html @@ -1,6 +1,6 @@
- {{ 'dashboard.trafficSummaryCard' | sqxTranslate }}: {{chartSummary | sqxFileSize}} + {{ 'dashboard.trafficHeader' | sqxTranslate }}: {{chartSummary | sqxFileSize}}
diff --git a/frontend/app/features/dashboard/pages/cards/api-traffic-summary-card.component.html b/frontend/app/features/dashboard/pages/cards/api-traffic-summary-card.component.html new file mode 100644 index 000000000..213b90b1d --- /dev/null +++ b/frontend/app/features/dashboard/pages/cards/api-traffic-summary-card.component.html @@ -0,0 +1,14 @@ +
+
{{ 'dashboard.trafficHeader' | sqxTranslate }}
+
+
+
{{ 'dashboard.currentMonthLabel' | sqxTranslate }}
+ +
{{bytesTotal | sqxFileSize}}
+ +
+ {{ 'dashboard.trafficLimitLabel' | sqxTranslate }}: {{bytesAllowed | sqxFileSize}} +
+
+
+
\ No newline at end of file diff --git a/frontend/app/features/dashboard/pages/cards/api-traffic-summary-card.component.scss b/frontend/app/features/dashboard/pages/cards/api-traffic-summary-card.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/app/features/dashboard/pages/cards/api-traffic-summary-card.component.ts b/frontend/app/features/dashboard/pages/cards/api-traffic-summary-card.component.ts new file mode 100644 index 000000000..ace8340d0 --- /dev/null +++ b/frontend/app/features/dashboard/pages/cards/api-traffic-summary-card.component.ts @@ -0,0 +1,37 @@ + +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; +import { AppDto, CallsUsageDto, fadeAnimation } from '@app/shared'; + +@Component({ + selector: 'sqx-api-traffic-summary-card', + styleUrls: ['./api-traffic-summary-card.component.scss'], + templateUrl: './api-traffic-summary-card.component.html', + animations: [ + fadeAnimation + ], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ApiTrafficSummaryCardComponent implements OnChanges { + @Input() + public app: AppDto; + + @Input() + public usage: CallsUsageDto; + + public bytesTotal = 0; + public bytesAllowed = 0; + + public ngOnChanges() { + if (this.usage) { + this.bytesTotal = this.usage.totalBytes; + this.bytesAllowed = this.usage.allowedBytes; + } + } +} \ No newline at end of file diff --git a/frontend/app/features/dashboard/pages/cards/github-card.component.html b/frontend/app/features/dashboard/pages/cards/github-card.component.html index 1b7c1f0f6..b6ae51dd2 100644 --- a/frontend/app/features/dashboard/pages/cards/github-card.component.html +++ b/frontend/app/features/dashboard/pages/cards/github-card.component.html @@ -5,7 +5,7 @@

- {{ 'dashboard.github' | sqxTranslate }} + {{ 'dashboard.githubCard' | sqxTranslate }}

diff --git a/frontend/app/features/dashboard/pages/dashboard-config.component.ts b/frontend/app/features/dashboard/pages/dashboard-config.component.ts index 93bf6889d..f460ffe76 100644 --- a/frontend/app/features/dashboard/pages/dashboard-config.component.ts +++ b/frontend/app/features/dashboard/pages/dashboard-config.component.ts @@ -107,10 +107,10 @@ export class DashboardConfigComponent implements OnChanges { } const DEFAULT_CONFIG: GridsterItem[] = [ - { cols: 1, rows: 1, x: 0, y: 0, type: 'schemas', name: 'i18n:common.schemas' }, - { cols: 1, rows: 1, x: 1, y: 0, type: 'api', name: 'i18n:dashboard.apiDocumentation' }, - { cols: 1, rows: 1, x: 2, y: 0, type: 'support', name: 'i18n:common.schemas' }, - { cols: 1, rows: 1, x: 3, y: 0, type: 'github', name: 'i18n:dashboard.github' }, + { cols: 1, rows: 1, x: 0, y: 0, type: 'schemas', name: 'i18n:dashboard.schemasCard' }, + { cols: 1, rows: 1, x: 1, y: 0, type: 'api', name: 'i18n:dashboard.apiDocumentationCard' }, + { cols: 1, rows: 1, x: 2, y: 0, type: 'support', name: 'i18n:dashboard.supportCard' }, + { cols: 1, rows: 1, x: 3, y: 0, type: 'github', name: 'i18n:dashboard.githubCard' }, { cols: 2, rows: 1, x: 0, y: 1, type: 'api-calls', name: 'i18n:dashboard.apiCallsChart' }, { cols: 2, rows: 1, x: 2, y: 1, type: 'api-performance', name: 'i18n:dashboard.apiPerformanceChart' }, @@ -122,5 +122,6 @@ const DEFAULT_CONFIG: GridsterItem[] = [ { cols: 2, rows: 1, x: 0, y: 3, type: 'asset-uploads-size', name: 'i18n:dashboard.assetTotalSize' }, { cols: 2, rows: 1, x: 2, y: 3, type: 'api-traffic', name: 'i18n:dashboard.trafficChart' }, - { cols: 2, rows: 1, x: 0, y: 4, type: 'history', name: 'i18n:dashboard.history' } + { cols: 1, rows: 1, x: 0, y: 4, type: 'api-traffic-summary', name: 'i18n:dashboard.trafficSummaryCard' }, + { cols: 2, rows: 1, x: 1, y: 4, type: 'history', name: 'i18n:dashboard.historyCard' } ]; \ No newline at end of file diff --git a/frontend/app/features/dashboard/pages/dashboard-page.component.html b/frontend/app/features/dashboard/pages/dashboard-page.component.html index 3ab415c1f..fd37660c8 100644 --- a/frontend/app/features/dashboard/pages/dashboard-page.component.html +++ b/frontend/app/features/dashboard/pages/dashboard-page.component.html @@ -12,61 +12,56 @@ - - + - - + - - + - - + - - + - + - - + - - + - - + - - + - + + + + - - + - - + - - + diff --git a/frontend/app/features/settings/pages/plans/plan.component.html b/frontend/app/features/settings/pages/plans/plan.component.html index 515f58dcd..388c0598c 100644 --- a/frontend/app/features/settings/pages/plans/plan.component.html +++ b/frontend/app/features/settings/pages/plans/plan.component.html @@ -10,6 +10,9 @@
{{planInfo.plan.maxApiCalls | sqxKNumber}} {{ 'plans.includedCalls' | sqxTranslate }}
+
+ {{planInfo.plan.maxApiBytes | sqxFileSize}} {{ 'plans.includedTraffic' | sqxTranslate }} +
{{planInfo.plan.maxAssetSize | sqxFileSize}} {{ 'plans.includedStorage' | sqxTranslate }}
diff --git a/frontend/app/shared/components/notifo.component.ts b/frontend/app/shared/components/notifo.component.ts index 5a7567d4f..0e9ec074c 100644 --- a/frontend/app/shared/components/notifo.component.ts +++ b/frontend/app/shared/components/notifo.component.ts @@ -28,6 +28,10 @@ export class NotifoComponent implements AfterViewInit, OnChanges, OnDestroy { @ViewChild('element', { static: false }) public element: ElementRef; + public get isConfigured() { + return !!this.notifoApiKey && !!this.notifoApiUrl; + } + public get showOnboarding() { return !!this.notifoApiUrl && !!this.topic; } @@ -38,7 +42,7 @@ export class NotifoComponent implements AfterViewInit, OnChanges, OnDestroy { this.notifoApiKey = authService.user?.notifoToken; this.notifoApiUrl = uiOptions.get('more.notifoApi'); - if (this.notifoApiKey && this.notifoApiUrl) { + if (this.isConfigured) { if (this.notifoApiUrl.indexOf('localhost:5002') >= 0) { resourceLoader.loadScript(`https://localhost:3002/notifo-sdk.js`); } else { @@ -48,15 +52,13 @@ export class NotifoComponent implements AfterViewInit, OnChanges, OnDestroy { } public ngAfterViewInit() { - const userToken = this.notifoApiKey; - - if (userToken) { + if (this.isConfigured) { let notifo = window['notifo']; - if (!notifo && this.notifoApiUrl) { + if (!notifo) { notifo = []; - const options: any = { apiUrl: this.notifoApiUrl, userToken }; + const options: any = { apiUrl: this.notifoApiUrl, userToken: this.notifoApiKey }; if (this.notifoApiUrl.indexOf('localhost:5002') >= 0) { options.styleUrl = 'https://localhost:3002/notifo-sdk.css'; diff --git a/frontend/app/shared/services/plans.service.spec.ts b/frontend/app/shared/services/plans.service.spec.ts index f614fa24b..c4af8d602 100644 --- a/frontend/app/shared/services/plans.service.spec.ts +++ b/frontend/app/shared/services/plans.service.spec.ts @@ -55,6 +55,7 @@ describe('PlansService', () => { yearlyId: 'free_yearly', yearlyCosts: '120 €', yearlyConfirmText: 'Change for 120 € per year?', + maxApiBytes: 128, maxApiCalls: 1000, maxAssetSize: 1500, maxContributors: 2500 @@ -67,6 +68,7 @@ describe('PlansService', () => { yearlyId: 'professional_yearly', yearlyCosts: '160 €', yearlyConfirmText: 'Change for 160 € per year?', + maxApiBytes: 512, maxApiCalls: 4000, maxAssetSize: 5500, maxContributors: 6500 @@ -89,13 +91,13 @@ describe('PlansService', () => { 'Change for 14 € per month?', 'free_yearly', '120 €', 'Change for 120 € per year?', - 1000, 1500, 2500), + 128, 1000, 1500, 2500), new PlanDto( 'professional', 'Professional', '18 €', 'Change for 18 € per month?', 'professional_yearly', '160 €', 'Change for 160 € per year?', - 4000, 5500, 6500) + 512, 4000, 5500, 6500) ], hasPortal: true }, diff --git a/frontend/app/shared/services/plans.service.ts b/frontend/app/shared/services/plans.service.ts index 0fc117148..e66faf5e8 100644 --- a/frontend/app/shared/services/plans.service.ts +++ b/frontend/app/shared/services/plans.service.ts @@ -27,6 +27,7 @@ export class PlanDto { public readonly yearlyId: string, public readonly yearlyCosts: string, public readonly yearlyConfirmText: string | undefined, + public readonly maxApiBytes: number, public readonly maxApiCalls: number, public readonly maxAssetSize: number, public readonly maxContributors: number @@ -72,6 +73,7 @@ export class PlansService { item.yearlyId, item.yearlyCosts, item.yearlyConfirmText, + item.maxApiBytes, item.maxApiCalls, item.maxAssetSize, item.maxContributors)), diff --git a/frontend/app/shared/services/usages.service.spec.ts b/frontend/app/shared/services/usages.service.spec.ts index 36660bb2f..dd8decbc7 100644 --- a/frontend/app/shared/services/usages.service.spec.ts +++ b/frontend/app/shared/services/usages.service.spec.ts @@ -41,7 +41,9 @@ describe('UsagesService', () => { expect(req.request.headers.get('If-Match')).toBeNull(); req.flush({ + allowedBytes: 512, allowedCalls: 100, + blockingCalls: 200, totalBytes: 1024, totalCalls: 40, averageElapsedMs: 12.4, @@ -64,7 +66,7 @@ describe('UsagesService', () => { }); expect(usages!).toEqual( - new CallsUsageDto(100, 1024, 40, 12.4, { + new CallsUsageDto(512, 100, 200, 1024, 40, 12.4, { category1: [ new CallsUsagePerDateDto(DateTime.parseISO('2017-10-12'), 10, 130, 12.3), new CallsUsagePerDateDto(DateTime.parseISO('2017-10-13'), 13, 170, 33.3) diff --git a/frontend/app/shared/services/usages.service.ts b/frontend/app/shared/services/usages.service.ts index 1e0fedc0b..d79d944ea 100644 --- a/frontend/app/shared/services/usages.service.ts +++ b/frontend/app/shared/services/usages.service.ts @@ -13,7 +13,9 @@ import { map } from 'rxjs/operators'; export class CallsUsageDto { constructor( + public readonly allowedBytes: number, public readonly allowedCalls: number, + public readonly blockingCalls: number, public readonly totalBytes: number, public readonly totalCalls: number, public readonly averageElapsedMs: number, @@ -95,7 +97,9 @@ export class UsagesService { const usages = new CallsUsageDto( + body.allowedBytes, body.allowedCalls, + body.blockingCalls, body.totalBytes, body.totalCalls, body.averageElapsedMs, diff --git a/frontend/app/shared/state/plans.state.spec.ts b/frontend/app/shared/state/plans.state.spec.ts index 6eb39eee0..ee1b4cc4d 100644 --- a/frontend/app/shared/state/plans.state.spec.ts +++ b/frontend/app/shared/state/plans.state.spec.ts @@ -25,8 +25,8 @@ describe('PlansState', () => { currentPlanId: 'id1', planOwner: creator, plans: [ - new PlanDto('id1', 'name1', '100€', undefined, 'id1_yearly', '200€', undefined, 1, 1, 1), - new PlanDto('id2', 'name2', '400€', undefined, 'id2_yearly', '800€', undefined, 2, 2, 2) + new PlanDto('id1', 'name1', '100€', undefined, 'id1_yearly', '200€', undefined, 1, 1, 1, 1), + new PlanDto('id2', 'name2', '400€', undefined, 'id2_yearly', '800€', undefined, 2, 2, 2, 2) ], hasPortal: true };