Browse Source

Download Endpoint and UI page

pull/261/head
Sebastian Stehle 9 years ago
parent
commit
ab4bc05cd9
  1. 7
      src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs
  2. 2
      src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs
  3. 2
      src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs
  4. 2
      src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs
  5. 2
      src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrain.cs
  6. 18
      src/Squidex.Domain.Apps.Events/Contents/ContentScheduleItemRemoved.cs
  7. 2
      src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs
  8. 4
      src/Squidex.Infrastructure/States/Store.cs
  9. 12
      src/Squidex.Infrastructure/States/StoreExtensions.cs
  10. 16
      src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
  11. 34
      src/Squidex/Areas/Api/Controllers/Backup/BackupController.cs
  12. 4
      src/Squidex/Config/Domain/EntitiesServices.cs
  13. 5
      src/Squidex/Config/Domain/StoreServices.cs
  14. 1
      src/Squidex/app/features/settings/declarations.ts
  15. 6
      src/Squidex/app/features/settings/module.ts
  16. 34
      src/Squidex/app/features/settings/pages/backups/backups-page.component.html
  17. 2
      src/Squidex/app/features/settings/pages/backups/backups-page.component.scss
  18. 49
      src/Squidex/app/features/settings/pages/backups/backups-page.component.ts
  19. 8
      src/Squidex/app/features/settings/settings-area.component.html
  20. 1
      src/Squidex/app/shared/declarations-base.ts
  21. 2
      src/Squidex/app/shared/module.ts
  22. 76
      src/Squidex/app/shared/services/backups.service.ts

7
src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs

@ -14,6 +14,7 @@ using NodaTime;
using Orleans; using Orleans;
using Orleans.Concurrency; using Orleans.Concurrency;
using Squidex.Domain.Apps.Entities.Backup.State; using Squidex.Domain.Apps.Entities.Backup.State;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Assets;
@ -38,7 +39,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
private CancellationTokenSource currentTask; private CancellationTokenSource currentTask;
private BackupStateJob currentJob; private BackupStateJob currentJob;
private Guid appId; private Guid appId;
private BackupState state; private BackupState state = new BackupState();
private IPersistence<BackupState> persistence; private IPersistence<BackupState> persistence;
public BackupGrain( public BackupGrain(
@ -120,7 +121,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
await backupArchiveLocation.DeleteArchiveAsync(job.Id); await backupArchiveLocation.DeleteArchiveAsync(job.Id);
} }
public async Task StartNewAsync() public async Task RunAsync()
{ {
if (currentTask != null) if (currentTask != null)
{ {
@ -180,7 +181,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
{ {
await writer.WriteEventAsync(eventData); await writer.WriteEventAsync(eventData);
} }
}, "AppId", appId, null, currentTask.Token); }, SquidexHeaders.AppId, appId.ToString(), null, currentTask.Token);
} }
stream.Position = 0; stream.Position = 0;

2
src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs

@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
{ {
public interface IBackupGrain : IGrainWithGuidKey public interface IBackupGrain : IGrainWithGuidKey
{ {
Task StartNewAsync(); Task RunAsync();
Task DeleteAsync(Guid id); Task DeleteAsync(Guid id);

2
src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs

@ -13,6 +13,6 @@ namespace Squidex.Domain.Apps.Entities.Backup.State
public sealed class BackupState public sealed class BackupState
{ {
[JsonProperty] [JsonProperty]
public List<BackupStateJob> Jobs { get; set; } = new List<BackupStateJob>(); public List<BackupStateJob> Jobs { get; } = new List<BackupStateJob>();
} }
} }

2
src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs

@ -18,7 +18,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
{ {
var tempFile = GetTempFile(backupId); var tempFile = GetTempFile(backupId);
return Task.FromResult<Stream>(new FileStream(tempFile, FileMode.Open, FileAccess.ReadWrite)); return Task.FromResult<Stream>(new FileStream(tempFile, FileMode.Create, FileAccess.ReadWrite));
} }
public Task DeleteArchiveAsync(Guid backupId) public Task DeleteArchiveAsync(Guid backupId)

2
src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrain.cs

@ -26,6 +26,8 @@ namespace Squidex.Domain.Apps.Entities
{ {
@event.SetAppId(appEvent.AppId.Id); @event.SetAppId(appEvent.AppId.Id);
} }
base.RaiseEvent(@event);
} }
} }
} }

18
src/Squidex.Domain.Apps.Events/Contents/ContentScheduleItemRemoved.cs

@ -1,18 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Contents
{
[EventType(nameof(ContentStatusChanged))]
public sealed class ContentScheduleItemRemoved : ContentEvent
{
public Guid ScheduleItemId { get; set; }
}
}

2
src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs

@ -32,7 +32,7 @@ namespace Squidex.Infrastructure.States
public async Task<(T Value, long Version)> ReadAsync(TKey key) public async Task<(T Value, long Version)> ReadAsync(TKey key)
{ {
var existing = var existing =
await Collection.Find(x => Equals(x.Id, key)) await Collection.Find(x => x.Id.Equals(key))
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (existing != null) if (existing != null)

4
src/Squidex.Infrastructure/States/Store.cs

@ -32,12 +32,12 @@ namespace Squidex.Infrastructure.States
public IPersistence<TState> WithSnapshots<TState>(Type owner, TKey key, Func<TState, Task> applySnapshot) public IPersistence<TState> WithSnapshots<TState>(Type owner, TKey key, Func<TState, Task> applySnapshot)
{ {
return CreatePersistence<TState>(owner, key, PersistenceMode.Snapshots, applySnapshot, null); return CreatePersistence(owner, key, PersistenceMode.Snapshots, applySnapshot, null);
} }
public IPersistence<TState> WithSnapshotsAndEventSourcing<TState>(Type owner, TKey key, Func<TState, Task> applySnapshot, Func<Envelope<IEvent>, Task> applyEvent) public IPersistence<TState> WithSnapshotsAndEventSourcing<TState>(Type owner, TKey key, Func<TState, Task> applySnapshot, Func<Envelope<IEvent>, Task> applyEvent)
{ {
return CreatePersistence<TState>(owner, key, PersistenceMode.SnapshotsAndEventSourcing, applySnapshot, applyEvent); return CreatePersistence(owner, key, PersistenceMode.SnapshotsAndEventSourcing, applySnapshot, applyEvent);
} }
public IPersistence WithEventSourcing(Type owner, TKey key, Func<Envelope<IEvent>, Task> applyEvent) public IPersistence WithEventSourcing(Type owner, TKey key, Func<Envelope<IEvent>, Task> applyEvent)

12
src/Squidex.Infrastructure/States/StoreExtensions.cs

@ -21,12 +21,12 @@ namespace Squidex.Infrastructure.States
public static IPersistence<TState> WithSnapshots<TOwner, TState, TKey>(this IStore<TKey> store, TKey key, Func<TState, Task> applySnapshot) public static IPersistence<TState> WithSnapshots<TOwner, TState, TKey>(this IStore<TKey> store, TKey key, Func<TState, Task> applySnapshot)
{ {
return store.WithSnapshots<TState>(typeof(TOwner), key, applySnapshot); return store.WithSnapshots(typeof(TOwner), key, applySnapshot);
} }
public static IPersistence<TState> WithSnapshotsAndEventSourcing<TOwner, TState, TKey>(this IStore<TKey> store, TKey key, Func<TState, Task> applySnapshot, Func<Envelope<IEvent>, Task> applyEvent) public static IPersistence<TState> WithSnapshotsAndEventSourcing<TOwner, TState, TKey>(this IStore<TKey> store, TKey key, Func<TState, Task> applySnapshot, Func<Envelope<IEvent>, Task> applyEvent)
{ {
return store.WithSnapshotsAndEventSourcing<TState>(typeof(TOwner), key, applySnapshot, applyEvent); return store.WithSnapshotsAndEventSourcing(typeof(TOwner), key, applySnapshot, applyEvent);
} }
public static IPersistence WithEventSourcing<TKey>(this IStore<TKey> store, Type owner, TKey key, Action<Envelope<IEvent>> applyEvent) public static IPersistence WithEventSourcing<TKey>(this IStore<TKey> store, Type owner, TKey key, Action<Envelope<IEvent>> applyEvent)
@ -36,12 +36,12 @@ namespace Squidex.Infrastructure.States
public static IPersistence<TState> WithSnapshots<TState, TKey>(this IStore<TKey> store, Type owner, TKey key, Action<TState> applySnapshot) public static IPersistence<TState> WithSnapshots<TState, TKey>(this IStore<TKey> store, Type owner, TKey key, Action<TState> applySnapshot)
{ {
return store.WithSnapshots<TState>(owner, key, applySnapshot.ToAsync()); return store.WithSnapshots(owner, key, applySnapshot.ToAsync());
} }
public static IPersistence<TState> WithSnapshotsAndEventSourcing<TState, TKey>(this IStore<TKey> store, Type owner, TKey key, Action<TState> applySnapshot, Action<Envelope<IEvent>> applyEvent) public static IPersistence<TState> WithSnapshotsAndEventSourcing<TState, TKey>(this IStore<TKey> store, Type owner, TKey key, Action<TState> applySnapshot, Action<Envelope<IEvent>> applyEvent)
{ {
return store.WithSnapshotsAndEventSourcing<TState>(owner, key, applySnapshot.ToAsync(), applyEvent.ToAsync()); return store.WithSnapshotsAndEventSourcing(owner, key, applySnapshot.ToAsync(), applyEvent.ToAsync());
} }
public static IPersistence WithEventSourcing<TOwner, TKey>(this IStore<TKey> store, TKey key, Action<Envelope<IEvent>> applyEvent) public static IPersistence WithEventSourcing<TOwner, TKey>(this IStore<TKey> store, TKey key, Action<Envelope<IEvent>> applyEvent)
@ -51,12 +51,12 @@ namespace Squidex.Infrastructure.States
public static IPersistence<TState> WithSnapshots<TOwner, TState, TKey>(this IStore<TKey> store, TKey key, Action<TState> applySnapshot) public static IPersistence<TState> WithSnapshots<TOwner, TState, TKey>(this IStore<TKey> store, TKey key, Action<TState> applySnapshot)
{ {
return store.WithSnapshots<TState>(typeof(TOwner), key, applySnapshot.ToAsync()); return store.WithSnapshots(typeof(TOwner), key, applySnapshot.ToAsync());
} }
public static IPersistence<TState> WithSnapshotsAndEventSourcing<TOwner, TState, TKey>(this IStore<TKey> store, TKey key, Action<TState> applySnapshot, Action<Envelope<IEvent>> applyEvent) public static IPersistence<TState> WithSnapshotsAndEventSourcing<TOwner, TState, TKey>(this IStore<TKey> store, TKey key, Action<TState> applySnapshot, Action<Envelope<IEvent>> applyEvent)
{ {
return store.WithSnapshotsAndEventSourcing<TState>(typeof(TOwner), key, applySnapshot.ToAsync(), applyEvent.ToAsync()); return store.WithSnapshotsAndEventSourcing(typeof(TOwner), key, applySnapshot.ToAsync(), applyEvent.ToAsync());
} }
} }
} }

16
src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs

@ -27,18 +27,18 @@ namespace Squidex.Areas.Api.Controllers.Assets
[SwaggerTag(nameof(Assets))] [SwaggerTag(nameof(Assets))]
public sealed class AssetContentController : ApiController public sealed class AssetContentController : ApiController
{ {
private readonly IAssetStore assetStorage; private readonly IAssetStore assetStore;
private readonly IAssetRepository assetRepository; private readonly IAssetRepository assetRepository;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator; private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
public AssetContentController( public AssetContentController(
ICommandBus commandBus, ICommandBus commandBus,
IAssetStore assetStorage, IAssetStore assetStore,
IAssetRepository assetRepository, IAssetRepository assetRepository,
IAssetThumbnailGenerator assetThumbnailGenerator) IAssetThumbnailGenerator assetThumbnailGenerator)
: base(commandBus) : base(commandBus)
{ {
this.assetStorage = assetStorage; this.assetStore = assetStore;
this.assetRepository = assetRepository; this.assetRepository = assetRepository;
this.assetThumbnailGenerator = assetThumbnailGenerator; this.assetThumbnailGenerator = assetThumbnailGenerator;
} }
@ -53,7 +53,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// <param name="height">The target height of the asset, if it is an image.</param> /// <param name="height">The target height of the asset, if it is an image.</param>
/// <param name="mode">The resize mode when the width and height is defined.</param> /// <param name="mode">The resize mode when the width and height is defined.</param>
/// <returns> /// <returns>
/// 200 => Asset found and content or (resize) image returned. /// 200 => Asset found and content or (resized) image returned.
/// 404 => Asset or app not found. /// 404 => Asset or app not found.
/// </returns> /// </returns>
[HttpGet] [HttpGet]
@ -79,7 +79,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
try try
{ {
await assetStorage.DownloadAsync(assetId, entity.FileVersion, assetSuffix, bodyStream); await assetStore.DownloadAsync(assetId, entity.FileVersion, assetSuffix, bodyStream);
} }
catch (AssetNotFoundException) catch (AssetNotFoundException)
{ {
@ -87,13 +87,13 @@ namespace Squidex.Areas.Api.Controllers.Assets
{ {
using (var destinationStream = GetTempStream()) using (var destinationStream = GetTempStream())
{ {
await assetStorage.DownloadAsync(assetId, entity.FileVersion, null, sourceStream); await assetStore.DownloadAsync(assetId, entity.FileVersion, null, sourceStream);
sourceStream.Position = 0; sourceStream.Position = 0;
await assetThumbnailGenerator.CreateThumbnailAsync(sourceStream, destinationStream, width, height, mode); await assetThumbnailGenerator.CreateThumbnailAsync(sourceStream, destinationStream, width, height, mode);
destinationStream.Position = 0; destinationStream.Position = 0;
await assetStorage.UploadAsync(assetId, entity.FileVersion, assetSuffix, destinationStream); await assetStore.UploadAsync(assetId, entity.FileVersion, assetSuffix, destinationStream);
destinationStream.Position = 0; destinationStream.Position = 0;
await destinationStream.CopyToAsync(bodyStream); await destinationStream.CopyToAsync(bodyStream);
@ -103,7 +103,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
} }
else else
{ {
await assetStorage.DownloadAsync(assetId, entity.FileVersion, null, bodyStream); await assetStore.DownloadAsync(assetId, entity.FileVersion, null, bodyStream);
} }
}); });
} }

34
src/Squidex/Areas/Api/Controllers/Backup/BackupController.cs

@ -14,8 +14,10 @@ using NSwag.Annotations;
using Orleans; using Orleans;
using Squidex.Areas.Api.Controllers.Backup.Models; using Squidex.Areas.Api.Controllers.Backup.Models;
using Squidex.Domain.Apps.Entities.Backup; using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Tasks;
using Squidex.Pipeline; using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Backup namespace Squidex.Areas.Api.Controllers.Backup
@ -31,11 +33,13 @@ namespace Squidex.Areas.Api.Controllers.Backup
public class BackupController : ApiController public class BackupController : ApiController
{ {
private readonly IGrainFactory grainFactory; private readonly IGrainFactory grainFactory;
private readonly IAssetStore assetStore;
public BackupController(ICommandBus commandBus, IGrainFactory grainFactory) public BackupController(ICommandBus commandBus, IGrainFactory grainFactory, IAssetStore assetStore)
: base(commandBus) : base(commandBus)
{ {
this.grainFactory = grainFactory; this.grainFactory = grainFactory;
this.assetStore = assetStore;
} }
/// <summary> /// <summary>
@ -71,15 +75,33 @@ namespace Squidex.Areas.Api.Controllers.Backup
[Route("apps/{app}/backups/")] [Route("apps/{app}/backups/")]
[ProducesResponseType(typeof(List<BackupJobDto>), 200)] [ProducesResponseType(typeof(List<BackupJobDto>), 200)]
[ApiCosts(0)] [ApiCosts(0)]
public async Task<IActionResult> PostBackup(string app) public IActionResult PostBackup(string app)
{ {
var backupGrain = grainFactory.GetGrain<IBackupGrain>(App.Id); var backupGrain = grainFactory.GetGrain<IBackupGrain>(App.Id);
await backupGrain.StartNewAsync(); backupGrain.RunAsync().Forget();
return NoContent(); return NoContent();
} }
/// <summary>
/// Get the backup content.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The id of the asset.</param>
/// <returns>
/// 200 => Backup found and content returned.
/// 404 => Backup or app not found.
/// </returns>
[HttpGet]
[Route("apps/{app}/backups/{id}")]
[ProducesResponseType(200)]
[ApiCosts(0.5)]
public IActionResult GetBackupContent(string app, Guid id)
{
return new FileCallbackResult("application/zip", "Backup.zip", bodyStream => assetStore.DownloadAsync(id.ToString(), 0, null, bodyStream));
}
/// <summary> /// <summary>
/// Delete a backup. /// Delete a backup.
/// </summary> /// </summary>
@ -89,15 +111,15 @@ namespace Squidex.Areas.Api.Controllers.Backup
/// 204 => Backup started. /// 204 => Backup started.
/// 404 => Backup or app not found. /// 404 => Backup or app not found.
/// </returns> /// </returns>
[HttpPost] [HttpDelete]
[Route("apps/{app}/backups/{id}")] [Route("apps/{app}/backups/{id}")]
[ProducesResponseType(typeof(List<BackupJobDto>), 200)] [ProducesResponseType(typeof(List<BackupJobDto>), 200)]
[ApiCosts(0)] [ApiCosts(0)]
public async Task<IActionResult> PostBackup(string app, Guid id) public async Task<IActionResult> DeleteBackup(string app, Guid id)
{ {
var backupGrain = grainFactory.GetGrain<IBackupGrain>(App.Id); var backupGrain = grainFactory.GetGrain<IBackupGrain>(App.Id);
await backupGrain.StartNewAsync(); await backupGrain.DeleteAsync(id);
return NoContent(); return NoContent();
} }

4
src/Squidex/Config/Domain/EntitiesServices.cs

@ -19,6 +19,7 @@ using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Templates; using Squidex.Domain.Apps.Entities.Apps.Templates;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Edm; using Squidex.Domain.Apps.Entities.Contents.Edm;
@ -50,6 +51,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<CachingGraphQLService>() services.AddSingletonAs<CachingGraphQLService>()
.As<IGraphQLService>(); .As<IGraphQLService>();
services.AddSingletonAs<TempFolderBackupArchiveLocation>()
.As<IBackupArchiveLocation>();
services.AddSingletonAs<AppProvider>() services.AddSingletonAs<AppProvider>()
.As<IAppProvider>(); .As<IAppProvider>();

5
src/Squidex/Config/Domain/StoreServices.cs

@ -18,6 +18,7 @@ using Squidex.Domain.Apps.Entities.Apps.Repositories;
using Squidex.Domain.Apps.Entities.Apps.State; using Squidex.Domain.Apps.Entities.Apps.State;
using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Assets.State; using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Domain.Apps.Entities.Backup.State;
using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Contents.State; using Squidex.Domain.Apps.Entities.Contents.State;
using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.History;
@ -69,6 +70,10 @@ namespace Squidex.Config.Domain
.As<IMigrationStatus>() .As<IMigrationStatus>()
.As<IInitializable>(); .As<IInitializable>();
services.AddSingletonAs(c => new MongoSnapshotStore<BackupState, Guid>(mongoDatabase, c.GetRequiredService<JsonSerializer>()))
.As<ISnapshotStore<BackupState, Guid>>()
.As<IInitializable>();
services.AddSingletonAs(c => new MongoSnapshotStore<EventConsumerState, string>(mongoDatabase, c.GetRequiredService<JsonSerializer>())) services.AddSingletonAs(c => new MongoSnapshotStore<EventConsumerState, string>(mongoDatabase, c.GetRequiredService<JsonSerializer>()))
.As<ISnapshotStore<EventConsumerState, string>>() .As<ISnapshotStore<EventConsumerState, string>>()
.As<IInitializable>(); .As<IInitializable>();

1
src/Squidex/app/features/settings/declarations.ts

@ -5,6 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
export * from './pages/backups/backups-page.component';
export * from './pages/clients/client.component'; export * from './pages/clients/client.component';
export * from './pages/clients/clients-page.component'; export * from './pages/clients/clients-page.component';
export * from './pages/contributors/contributors-page.component'; export * from './pages/contributors/contributors-page.component';

6
src/Squidex/app/features/settings/module.ts

@ -17,6 +17,7 @@ import {
} from 'shared'; } from 'shared';
import { import {
BackupsPageComponent,
ClientComponent, ClientComponent,
ClientsPageComponent, ClientsPageComponent,
ContributorsPageComponent, ContributorsPageComponent,
@ -45,6 +46,10 @@ const routes: Routes = [
path: 'more', path: 'more',
component: MorePageComponent component: MorePageComponent
}, },
{
path: 'backups',
component: BackupsPageComponent
},
{ {
path: 'clients', path: 'clients',
component: ClientsPageComponent, component: ClientsPageComponent,
@ -137,6 +142,7 @@ const routes: Routes = [
RouterModule.forChild(routes) RouterModule.forChild(routes)
], ],
declarations: [ declarations: [
BackupsPageComponent,
ClientComponent, ClientComponent,
ClientsPageComponent, ClientsPageComponent,
ContributorsPageComponent, ContributorsPageComponent,

34
src/Squidex/app/features/settings/pages/backups/backups-page.component.html

@ -0,0 +1,34 @@
<sqx-title message="{app} | Backups | Settings" parameter1="app" [value1]="ctx.appName"></sqx-title>
<sqx-panel desiredWidth="50rem">
<div class="panel-header">
<div class="panel-title-row">
<div class="float-right">
<button class="btn btn-success" (click)="startBackup()">
Start Backup
</button>
</div>
<h3 class="panel-title">Backups</h3>
</div>
<a class="panel-close" sqxParentLink>
<i class="icon-close"></i>
</a>
</div>
<div class="panel-main">
<div class="panel-content">
<table class="table table-items table-fixed">
<tbody>
<ng-template ngFor let-backup [ngForOf]="backups | async">
<tr>
<td>{{backup.started | sqxFullDateTime }}</td>
</tr>
<tr class="spacer"></tr>
</ng-template>
</tbody>
</table>
</div>
</div>
</sqx-panel>

2
src/Squidex/app/features/settings/pages/backups/backups-page.component.scss

@ -0,0 +1,2 @@
@import '_vars';
@import '_mixins';

49
src/Squidex/app/features/settings/pages/backups/backups-page.component.ts

@ -0,0 +1,49 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { AppContext, BackupsService } from 'shared';
@Component({
selector: 'sqx-backups-page',
styleUrls: ['./backups-page.component.scss'],
templateUrl: './backups-page.component.html',
providers: [
AppContext
]
})
export class BackupsPageComponent {
public backups =
Observable.timer(0, 5000)
.switchMap(t => this.backupsService.getBackups(this.ctx.appName));
constructor(public readonly ctx: AppContext,
private readonly backupsService: BackupsService
) {
}
public startBackup() {
this.backupsService.postBackup(this.ctx.appName)
.subscribe(() => {
this.ctx.notifyInfo('Backup started.');
}, error => {
this.ctx.notifyError(error);
});
}
public deleteBackup(id: string) {
this.backupsService.deleteBackup(this.ctx.appName, id)
.subscribe(() => {
this.ctx.notifyInfo('Backup deleting.');
}, error => {
this.ctx.notifyError(error);
});
}
}

8
src/Squidex/app/features/settings/settings-area.component.html

@ -14,6 +14,12 @@
<div class="panel-main"> <div class="panel-main">
<div class="panel-content"> <div class="panel-content">
<ul class="nav nav-panel nav-dark flex-column"> <ul class="nav nav-panel nav-dark flex-column">
<li class="nav-item" *ngIf="ctx.app.permission === 'Owner'">
<a class="nav-link" routerLink="backups" routerLinkActive="active">
Backups
<i class="icon-angle-right"></i>
</a>
</li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" routerLink="clients" routerLinkActive="active"> <a class="nav-link" routerLink="clients" routerLinkActive="active">
Clients Clients
@ -40,7 +46,7 @@
</li> </li>
<li class="nav-item" *ngIf="ctx.app.permission === 'Owner'"> <li class="nav-item" *ngIf="ctx.app.permission === 'Owner'">
<a class="nav-link" routerLink="plans" routerLinkActive="active"> <a class="nav-link" routerLink="plans" routerLinkActive="active">
Update Plan Subscription
<i class="icon-angle-right"></i> <i class="icon-angle-right"></i>
</a> </a>
</li> </li>

1
src/Squidex/app/shared/declarations-base.ts

@ -25,6 +25,7 @@ export * from './services/apps-store.service';
export * from './services/apps.service'; export * from './services/apps.service';
export * from './services/assets.service'; export * from './services/assets.service';
export * from './services/auth.service'; export * from './services/auth.service';
export * from './services/backups.service';
export * from './services/contents.service'; export * from './services/contents.service';
export * from './services/event-consumers.service'; export * from './services/event-consumers.service';
export * from './services/graphql.service'; export * from './services/graphql.service';

2
src/Squidex/app/shared/module.ts

@ -26,6 +26,7 @@ import {
AssetUrlPipe, AssetUrlPipe,
AuthInterceptor, AuthInterceptor,
AuthService, AuthService,
BackupsService,
ContentsService, ContentsService,
EventConsumersService, EventConsumersService,
FileIconPipe, FileIconPipe,
@ -127,6 +128,7 @@ export class SqxSharedModule {
AppsStoreService, AppsStoreService,
AssetsService, AssetsService,
AuthService, AuthService,
BackupsService,
ContentsService, ContentsService,
EventConsumersService, EventConsumersService,
GraphQlService, GraphQlService,

76
src/Squidex/app/shared/services/backups.service.ts

@ -0,0 +1,76 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import 'framework/angular/http-extensions';
import {
AnalyticsService,
ApiUrlConfig,
DateTime
} from 'framework';
export class BackupDto {
constructor(
public readonly id: string,
public readonly isFailed: string,
public readonly started: DateTime,
public readonly stopped: DateTime | null
) {
}
}
@Injectable()
export class BackupsService {
constructor(
private readonly http: HttpClient,
private readonly apiUrl: ApiUrlConfig,
private readonly analytics: AnalyticsService
) {
}
public getBackups(appName: string): Observable<BackupDto[]> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/backups`);
return this.http.get(url)
.map(response => {
const items: any[] = <any>response;
return items.map(item => {
return new BackupDto(
item.id,
item.isFailed,
DateTime.parseISO_UTC(item.stopped),
item.stopped ? DateTime.parseISO_UTC(item.stopped) : null);
});
})
.pretifyError('Failed to load backups.');
}
public postBackup(appName: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/backups`);
return this.http.post(url, {})
.do(() => {
this.analytics.trackEvent('Backup', 'Started', appName);
})
.pretifyError('Failed to start backup.');
}
public deleteBackup(appName: string, id: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/backups/${id}`);
return this.http.delete(url)
.do(() => {
this.analytics.trackEvent('Backup', 'Deleted', appName);
})
.pretifyError('Failed to delete backup.');
}
}
Loading…
Cancel
Save