diff --git a/extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs b/extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs index 72eaa0db7..0c2bdd610 100644 --- a/extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs +++ b/extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs @@ -34,14 +34,14 @@ namespace Squidex.Extensions.Actions IconImage = "", IconColor = "#3389ff", Display = "Asset changed", - Description = "For asset changes like uploaded, updated, renamed, deleted..." + Description = "For asset changes like uploaded, updated (reuploaded), renamed, deleted..." }, [UsageTrigger.Name] = new RuleElement { IconImage = "", IconColor = "#3389ff", - Display = "Usage limitations", - Description = "When monthly API calls exceeed a specified limit..." + Display = "Usage exceeded", + Description = "When monthly API calls exceed a specified limit for one time a month..." } }; diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs index 812b2862b..87582bcce 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs @@ -9,17 +9,14 @@ using System; using System.Threading.Tasks; using Orleans; using Squidex.Infrastructure; +using Squidex.Infrastructure.Orleans; namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking { - public interface IUsageTrackerGrain : IGrainWithStringKey + public interface IUsageTrackerGrain : IGrainWithStringKey, IBackgroundGrain { Task AddTargetAsync(Guid ruleId, NamedId appId, int limits); - Task ActivateTargetAsync(Guid ruleId); - - Task DeactivateTargetAsync(Guid ruleId); - Task RemoveTargetAsync(Guid ruleId); Task UpdateTargetAsync(Guid ruleId, int limits); diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs index 3556a8f47..a287add24 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs @@ -34,12 +34,6 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking case DeleteRule deleteRule: await usageTrackerGrain.RemoveTargetAsync(deleteRule.RuleId); break; - case EnableRule enableRule: - await usageTrackerGrain.ActivateTargetAsync(enableRule.RuleId); - break; - case DisableRule disableRule: - await usageTrackerGrain.DeactivateTargetAsync(disableRule.RuleId); - break; case CreateRule createRule: { if (createRule.Trigger is UsageTrigger createdTrigger) diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs index c1cda0185..38b4cdcf8 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs @@ -9,16 +9,19 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Orleans; +using Orleans.Concurrency; using Orleans.Runtime; using Squidex.Domain.Apps.Events; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.States; +using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.UsageTracking; namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking { + [Reentrant] public sealed class UsageTrackerGrain : GrainOfString, IRemindable, IUsageTrackerGrain { private readonly IUsageTracker usageTracker; @@ -27,9 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking { public int Limits { get; set; } - public bool Disabled { get; set; } - - public DateTime Triggered { get; set; } + public DateTime? Triggered { get; set; } public NamedId AppId { get; set; } } @@ -53,11 +54,22 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking DelayDeactivation(TimeSpan.FromDays(1)); RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); + RegisterTimer(x => CheckUsagesAsync(), null, TimeSpan.Zero, TimeSpan.FromMinutes(10)); return Task.CompletedTask; } - public async Task ReceiveReminder(string reminderName, TickStatus status) + public Task ActivateAsync() + { + return TaskHelper.Done; + } + + public Task ReceiveReminder(string reminderName, TickStatus status) + { + return TaskHelper.Done; + } + + public async Task CheckUsagesAsync() { var today = DateTime.Today; @@ -65,7 +77,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking { var appId = kvp.Value.AppId; - if (!IsSameMonth(today, kvp.Value.Triggered)) + if (!kvp.Value.Triggered.HasValue || !IsSameMonth(today, kvp.Value.Triggered.Value)) { var usage = await usageTracker.GetMonthlyCallsAsync(appId.Id.ToString(), today); @@ -110,20 +122,6 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking return WriteStateAsync(); } - public Task ActivateTargetAsync(Guid ruleId) - { - UpdateTarget(ruleId, t => t.Disabled = false); - - return WriteStateAsync(); - } - - public Task DeactivateTargetAsync(Guid ruleId) - { - UpdateTarget(ruleId, t => t.Disabled = true); - - return WriteStateAsync(); - } - public Task AddTargetAsync(Guid ruleId, int limits) { UpdateTarget(ruleId, t => t.Limits = limits); diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs index f5b17df98..42bcb40e2 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTriggerHandler.cs @@ -16,7 +16,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking { public sealed class UsageTriggerHandler : RuleTriggerHandler { - private const string EventName = "Usage exceeeded"; + private const string EventName = "Usage exceeded"; protected override Task CreateEnrichedEventAsync(Envelope @event) { diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs index 3b74047de..25fb14496 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Converters/RuleTriggerDtoFactory.cs @@ -33,7 +33,7 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Converters public RuleTriggerDto Visit(UsageTrigger trigger) { - return SimpleMapper.Map(trigger, new UsageTriggerDto()); + return SimpleMapper.Map(trigger, new UsageRuleTriggerDto()); } public RuleTriggerDto Visit(ContentChangedTriggerV2 trigger) diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageTriggerDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs similarity index 93% rename from src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageTriggerDto.cs rename to src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs index f298b6ef6..00cc24b41 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageTriggerDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs @@ -11,7 +11,7 @@ using Squidex.Infrastructure.Reflection; namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers { - public sealed class UsageTriggerDto : RuleTriggerDto + public sealed class UsageRuleTriggerDto : RuleTriggerDto { /// /// The number of monthly api calls. diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs index faa19dd39..4401d09b8 100644 --- a/src/Squidex/Config/Domain/EntitiesServices.cs +++ b/src/Squidex/Config/Domain/EntitiesServices.cs @@ -35,6 +35,7 @@ using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Domain.Apps.Entities.Rules.Indexes; +using Squidex.Domain.Apps.Entities.Rules.UsageTracking; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Domain.Apps.Entities.Schemas.Indexes; @@ -207,6 +208,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + + services.AddSingletonAs() + .As(); } private static void AddBackupHandlers(this IServiceCollection services) diff --git a/src/Squidex/Config/Orleans/SiloWrapper.cs b/src/Squidex/Config/Orleans/SiloWrapper.cs index 334993f1e..55f8b6d11 100644 --- a/src/Squidex/Config/Orleans/SiloWrapper.cs +++ b/src/Squidex/Config/Orleans/SiloWrapper.cs @@ -18,6 +18,7 @@ using Orleans.Configuration; using Orleans.Hosting; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Rules.UsageTracking; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing.Grains; using Squidex.Infrastructure.Log; @@ -68,6 +69,7 @@ namespace Squidex.Config.Orleans .AddStartupTask>() .AddStartupTask>() .AddStartupTask>() + .AddStartupTask>() .Configure(options => { options.Configure(); diff --git a/src/Squidex/app/features/rules/declarations.ts b/src/Squidex/app/features/rules/declarations.ts index c14638e0f..eb57c8359 100644 --- a/src/Squidex/app/features/rules/declarations.ts +++ b/src/Squidex/app/features/rules/declarations.ts @@ -19,6 +19,7 @@ export * from './pages/rules/actions/webhook-action.component'; export * from './pages/rules/triggers/asset-changed-trigger.component'; export * from './pages/rules/triggers/content-changed-trigger.component'; +export * from './pages/rules/triggers/usage-trigger.component'; export * from './pages/rules/rule-element.component'; export * from './pages/rules/rule-wizard.component'; diff --git a/src/Squidex/app/features/rules/module.ts b/src/Squidex/app/features/rules/module.ts index 99a9f48db..55d204251 100644 --- a/src/Squidex/app/features/rules/module.ts +++ b/src/Squidex/app/features/rules/module.ts @@ -32,6 +32,7 @@ import { RuleWizardComponent, SlackActionComponent, TweetActionComponent, + UsageTriggerComponent, WebhookActionComponent } from './declarations'; @@ -79,6 +80,7 @@ const routes: Routes = [ RuleWizardComponent, SlackActionComponent, TweetActionComponent, + UsageTriggerComponent, WebhookActionComponent ] }) diff --git a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html index c9b29629e..54a41851e 100644 --- a/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html +++ b/src/Squidex/app/features/rules/pages/rules/rule-wizard.component.html @@ -49,6 +49,13 @@ [triggerFormSubmitted]="triggerForm.submitted | async"> + + + + diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.html b/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.html new file mode 100644 index 000000000..9b18d5467 --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.html @@ -0,0 +1,13 @@ +
+
+ + + + + + + + The monthly api calls to trigger. + +
+
\ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.scss b/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.scss new file mode 100644 index 000000000..6e1eef5ec --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.scss @@ -0,0 +1,6 @@ +@import '_vars'; +@import '_mixins'; + +textarea { + height: 100px; +} \ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.ts b/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.ts new file mode 100644 index 000000000..d08c1d15c --- /dev/null +++ b/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.ts @@ -0,0 +1,30 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Component, Input, OnInit } from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; + +@Component({ + selector: 'sqx-usage-trigger', + styleUrls: ['./usage-trigger.component.scss'], + templateUrl: './usage-trigger.component.html' +}) +export class UsageTriggerComponent implements OnInit { + @Input() + public trigger: any; + + @Input() + public triggerForm: FormGroup; + + @Input() + public triggerFormSubmitted = false; + + public ngOnInit() { + this.triggerForm.setControl('limit', + new FormControl(this.trigger.limit || 20000)); + } +} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs similarity index 98% rename from tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerTests.cs rename to tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs index 19b3f2fd7..584a1c50e 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs @@ -25,21 +25,21 @@ using Xunit; namespace Squidex.Domain.Apps.Entities.Assets { - public class AssetChangedTriggerTests + public class AssetChangedTriggerHandlerTests { private readonly IScriptEngine scriptEngine = A.Fake(); private readonly IGrainFactory grainFactory = A.Fake(); private readonly IRuleTriggerHandler sut; - public AssetChangedTriggerTests() + public AssetChangedTriggerHandlerTests() { - sut = new AssetChangedTriggerHandler(scriptEngine, grainFactory); - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "true")) .Returns(true); A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "false")) .Returns(false); + + sut = new AssetChangedTriggerHandler(scriptEngine, grainFactory); } public static IEnumerable TestEvents = new[] diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs similarity index 98% rename from tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerTests.cs rename to tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs index a2c601af5..03b547993 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs @@ -29,7 +29,7 @@ using Xunit; namespace Squidex.Domain.Apps.Entities.Contents { - public class ContentChangedTriggerTests + public class ContentChangedTriggerHandlerTests { private readonly IScriptEngine scriptEngine = A.Fake(); private readonly IGrainFactory grainFactory = A.Fake(); @@ -38,15 +38,15 @@ namespace Squidex.Domain.Apps.Entities.Contents private static readonly NamedId SchemaMatch = NamedId.Of(Guid.NewGuid(), "my-schema1"); private static readonly NamedId SchemaNonMatch = NamedId.Of(Guid.NewGuid(), "my-schema2"); - public ContentChangedTriggerTests() + public ContentChangedTriggerHandlerTests() { - sut = new ContentChangedTriggerHandler(scriptEngine, grainFactory); - A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "true")) .Returns(true); A.CallTo(() => scriptEngine.Evaluate("event", A.Ignored, "false")) .Returns(false); + + sut = new ContentChangedTriggerHandler(scriptEngine, grainFactory); } public static IEnumerable TestEvents = new[]