feat: 添加WebHook功能
This commit is contained in:
parent
a29948587c
commit
b14dcd9d21
@ -31,10 +31,7 @@ public partial class RepositoryModel : ReactiveModel
|
|||||||
|
|
||||||
public ObservableCollection<Commit> Commits { get; } = new();
|
public ObservableCollection<Commit> Commits { get; } = new();
|
||||||
|
|
||||||
// todo cache depots?
|
public ObservableCollection<Webhook> Webhooks { get; } = new();
|
||||||
public ObservableCollection<Depot> Depots { get; } = new();
|
|
||||||
|
|
||||||
public ObservableCollection<Lock> Locks { get; } = new();
|
|
||||||
|
|
||||||
public ObservableCollection<Issue> Issues { get; } = new();
|
public ObservableCollection<Issue> Issues { get; } = new();
|
||||||
|
|
||||||
@ -45,7 +42,20 @@ public partial class RepositoryModel : ReactiveModel
|
|||||||
Developer = 2,
|
Developer = 2,
|
||||||
Owner = 3,
|
Owner = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public partial class Webhook : ReactiveModel
|
||||||
|
{
|
||||||
|
[Reactive] private int _id;
|
||||||
|
|
||||||
|
[Reactive] private string _targetUrl;
|
||||||
|
|
||||||
|
[Reactive] private bool _active;
|
||||||
|
|
||||||
|
[Reactive] private DateTime _createdAt;
|
||||||
|
|
||||||
|
[Reactive] private WebhookEventType _eventType;
|
||||||
|
}
|
||||||
|
|
||||||
public partial class Issue : ReactiveModel
|
public partial class Issue : ReactiveModel
|
||||||
{
|
{
|
||||||
[Reactive] private ulong _id;
|
[Reactive] private ulong _id;
|
||||||
|
|||||||
@ -572,6 +572,8 @@ public class RepositoryService : BaseService<RepositoryService>
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public async ValueTask<bool> UpdateIssueAsync(RepositoryModel repo, ulong issueId, string? title, string? description, IEnumerable<string>? tags)
|
public async ValueTask<bool> UpdateIssueAsync(RepositoryModel repo, ulong issueId, string? title, string? description, IEnumerable<string>? tags)
|
||||||
{
|
{
|
||||||
@ -1382,4 +1384,126 @@ public class RepositoryService : BaseService<RepositoryService>
|
|||||||
|
|
||||||
return depotFile;
|
return depotFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async ValueTask<bool> UpdateWebhooksFromServerAsync(RepositoryModel repo)
|
||||||
|
{
|
||||||
|
var api = Api.C;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
|
||||||
|
{
|
||||||
|
api.ClearGateway();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var webhooks = await api.Gateway.WebhooksGet(repo.OwnerName, repo.Name);
|
||||||
|
|
||||||
|
var dict = webhooks.ToDictionary(w => w.Id);
|
||||||
|
for (var i = 0; i < repo.Webhooks.Count; i++)
|
||||||
|
{
|
||||||
|
var wh = repo.Webhooks[i];
|
||||||
|
if (!dict.Remove(wh.Id, out var newWh))
|
||||||
|
{
|
||||||
|
repo.Webhooks.RemoveAt(i);
|
||||||
|
i -= 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
wh.TargetUrl = newWh.TargetUrl;
|
||||||
|
wh.Active = newWh.IsActive;
|
||||||
|
wh.EventType = newWh.EventType;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var wh in dict.Values)
|
||||||
|
{
|
||||||
|
repo.Webhooks.Add(new RepositoryModel.Webhook
|
||||||
|
{
|
||||||
|
Id = wh.Id,
|
||||||
|
TargetUrl = wh.TargetUrl,
|
||||||
|
Active = wh.IsActive,
|
||||||
|
CreatedAt = wh.CreatedAt.UtcDateTime,
|
||||||
|
EventType = wh.EventType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
UIHelper.NotifyError(e);
|
||||||
|
Console.WriteLine(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<bool> AddWebhookAsync(RepositoryModel repo, string targetUrl, string secret, WebhookEventType evt)
|
||||||
|
{
|
||||||
|
var api = Api.C;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
|
||||||
|
{
|
||||||
|
api.ClearGateway();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.Gateway.Create(repo.OwnerName, repo.Name,
|
||||||
|
new WebhookCreateRequest {
|
||||||
|
TargetUrl = targetUrl,
|
||||||
|
Secret = secret,
|
||||||
|
EventType = evt,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await UpdateWebhooksFromServerAsync(repo);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
UIHelper.NotifyError(e);
|
||||||
|
Console.WriteLine(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<bool> DeleteWebhookAsync(RepositoryModel repo, int webhookId)
|
||||||
|
{
|
||||||
|
var api = Api.C;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
|
||||||
|
{
|
||||||
|
api.ClearGateway();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.Gateway.WebhooksDelete(repo.OwnerName, repo.Name, webhookId);
|
||||||
|
return await UpdateWebhooksFromServerAsync(repo);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
UIHelper.NotifyError(e);
|
||||||
|
Console.WriteLine(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<bool> ToggleWebhookAsync(RepositoryModel repo, int webhookId, bool activate)
|
||||||
|
{
|
||||||
|
var api = Api.C;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
|
||||||
|
{
|
||||||
|
api.ClearGateway();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.Gateway.Toggle(repo.OwnerName, repo.Name, webhookId, activate);
|
||||||
|
return await UpdateWebhooksFromServerAsync(repo);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
UIHelper.NotifyError(e);
|
||||||
|
Console.WriteLine(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
using Flawless.Client.Remote;
|
||||||
|
using ReactiveUI;
|
||||||
|
using ReactiveUI.SourceGenerators;
|
||||||
|
|
||||||
|
namespace Flawless.Client.ViewModels.ModalBox;
|
||||||
|
|
||||||
|
public partial class WebhookEditDialogViewModel : ReactiveObject
|
||||||
|
{
|
||||||
|
[Reactive] private string _url;
|
||||||
|
|
||||||
|
[Reactive] private string _secret;
|
||||||
|
|
||||||
|
[Reactive] private WebhookEventType _eventType;
|
||||||
|
|
||||||
|
}
|
||||||
@ -137,11 +137,9 @@ public partial class RepositoryViewModel : RoutableViewModelBase
|
|||||||
|
|
||||||
public UserModel User { get; }
|
public UserModel User { get; }
|
||||||
|
|
||||||
public ServerStatusResponse ServerStatus => Api.C.Status.Value!;
|
|
||||||
|
|
||||||
[Reactive] private bool _autoDetectChanges = true;
|
[Reactive] private bool _autoDetectChanges = true;
|
||||||
|
|
||||||
[Reactive] private bool _isOwnerRole, _isDeveloperRole, _isReporterRole, _isGuestRole;
|
[Reactive] private bool _isOwnerRole, _isDeveloperRole, _isReporterRole, _isGuestRole, _showWebHook;
|
||||||
|
|
||||||
private string _wsRoot;
|
private string _wsRoot;
|
||||||
|
|
||||||
@ -241,10 +239,13 @@ public partial class RepositoryViewModel : RoutableViewModelBase
|
|||||||
var path = Path.Combine(_wsRoot, node.FullPath);
|
var path = Path.Combine(_wsRoot, node.FullPath);
|
||||||
return (ulong)new FileInfo(path).Length;
|
return (ulong)new FileInfo(path).Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task StartupTasksAsync()
|
private async Task StartupTasksAsync()
|
||||||
{
|
{
|
||||||
await RefreshRepositoryRoleInfoAsyncCommand.Execute();
|
await RefreshRepositoryRoleInfoAsyncCommand.Execute();
|
||||||
|
await RefreshRepositoryIssuesAsyncCommand.Execute();
|
||||||
|
await RefreshWebhooksCommand.Execute();
|
||||||
await DetectLocalChangesAsyncCommand.Execute();
|
await DetectLocalChangesAsyncCommand.Execute();
|
||||||
await RendererFileTreeAsync();
|
await RendererFileTreeAsync();
|
||||||
SyncCommitsFromRepository();
|
SyncCommitsFromRepository();
|
||||||
@ -336,6 +337,48 @@ public partial class RepositoryViewModel : RoutableViewModelBase
|
|||||||
if (role >= RepositoryModel.RepositoryRole.Reporter) IsReporterRole = true;
|
if (role >= RepositoryModel.RepositoryRole.Reporter) IsReporterRole = true;
|
||||||
if (role >= RepositoryModel.RepositoryRole.Guest) IsGuestRole = true;
|
if (role >= RepositoryModel.RepositoryRole.Guest) IsGuestRole = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[ReactiveCommand]
|
||||||
|
private async Task AddWebhookAsync()
|
||||||
|
{
|
||||||
|
if (!IsOwnerRole) return;
|
||||||
|
|
||||||
|
var vm = new WebhookEditDialogViewModel();
|
||||||
|
var result = await OverlayDialog.ShowModal<WebhookEditDialogView, WebhookEditDialogViewModel>(
|
||||||
|
vm, AppDefaultValues.HostId, UIHelper.DefaultOverlayDialogOptionsYesNo());
|
||||||
|
|
||||||
|
if (result == DialogResult.Yes)
|
||||||
|
{
|
||||||
|
using var l = UIHelper.MakeLoading("Create Webhook...");
|
||||||
|
await RepositoryService.C.AddWebhookAsync(Repository, vm.Url, vm.Secret, vm.EventType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ReactiveCommand]
|
||||||
|
private async Task DeleteWebhookAsync(Webhook webhook)
|
||||||
|
{
|
||||||
|
if (!IsOwnerRole) return;
|
||||||
|
|
||||||
|
var confirm = await UIHelper.SimpleAskAsync($"Do you sure to delete webhook with url at {webhook.TargetUrl} ?");
|
||||||
|
if (confirm == DialogResult.Yes)
|
||||||
|
{
|
||||||
|
using var l = UIHelper.MakeLoading("Delete webhook...");
|
||||||
|
await RepositoryService.C.DeleteWebhookAsync(Repository, webhook.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ReactiveCommand]
|
||||||
|
public async Task ToggleWebhookAsync(Webhook webhook)
|
||||||
|
{
|
||||||
|
if (!IsOwnerRole) return;
|
||||||
|
|
||||||
|
using var l = UIHelper.MakeLoading("Update Webhook...");
|
||||||
|
var success = await RepositoryService.C.ToggleWebhookAsync(
|
||||||
|
Repository,
|
||||||
|
webhook.Id,
|
||||||
|
!webhook.IsActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[ReactiveCommand]
|
[ReactiveCommand]
|
||||||
private async Task RevertFileTreeToSelectedCommitKeepAsync()
|
private async Task RevertFileTreeToSelectedCommitKeepAsync()
|
||||||
@ -562,6 +605,15 @@ public partial class RepositoryViewModel : RoutableViewModelBase
|
|||||||
await RepositoryService.C.CloseRepositoryAsync(Repository);
|
await RepositoryService.C.CloseRepositoryAsync(Repository);
|
||||||
await HostScreen.Router.NavigateBack.Execute();
|
await HostScreen.Router.NavigateBack.Execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[ReactiveCommand]
|
||||||
|
private async Task RefreshWebhooksAsync()
|
||||||
|
{
|
||||||
|
ShowWebHook = IsDeveloperRole && (Api.C.Status.Value?.AllowWebHook ?? false);
|
||||||
|
if (!ShowWebHook) return;
|
||||||
|
using var l = UIHelper.MakeLoading("刷新Webhook列表...");
|
||||||
|
await RepositoryService.C.UpdateWebhooksFromServerAsync(Repository);
|
||||||
|
}
|
||||||
|
|
||||||
[ReactiveCommand]
|
[ReactiveCommand]
|
||||||
private async ValueTask RefreshRepositoryRoleInfoAsync()
|
private async ValueTask RefreshRepositoryRoleInfoAsync()
|
||||||
|
|||||||
27
Flawless.Client/Views/ModalBox/WebhookEditDialogView.axaml
Normal file
27
Flawless.Client/Views/ModalBox/WebhookEditDialogView.axaml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:u="https://irihi.tech/ursa"
|
||||||
|
xmlns:vm="using:Flawless.Client.ViewModels.ModalBox"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Flawless.Client.Views.ModalBox.WebhookEditDialogView"
|
||||||
|
x:DataType="vm:WebhookEditDialogViewModel">
|
||||||
|
|
||||||
|
<u:Form HorizontalAlignment="Stretch" LabelPosition="Top">
|
||||||
|
<u:Form.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<Grid RowDefinitions="*, *, *" />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</u:Form.ItemsPanel>
|
||||||
|
<u:FormItem Grid.Row="0" Label="URL">
|
||||||
|
<TextBox Text="{Binding Url}"/>
|
||||||
|
</u:FormItem>
|
||||||
|
<u:FormItem Grid.Row="1" Label="Secret">
|
||||||
|
<TextBox Text="{Binding Secret}"/>
|
||||||
|
</u:FormItem>
|
||||||
|
<u:FormItem Grid.Row="1" Label="Event Type">
|
||||||
|
<u:EnumSelector SelectedValue="{Binding EventType}"/>
|
||||||
|
</u:FormItem>
|
||||||
|
</u:Form>
|
||||||
|
</UserControl>
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
|
namespace Flawless.Client.Views.ModalBox;
|
||||||
|
|
||||||
|
public partial class WebhookEditDialogView : UserControl
|
||||||
|
{
|
||||||
|
public WebhookEditDialogView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@
|
|||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:u="https://irihi.tech/ursa"
|
xmlns:u="https://irihi.tech/ursa"
|
||||||
xmlns:vm="using:Flawless.Client.ViewModels"
|
xmlns:vm="using:Flawless.Client.ViewModels"
|
||||||
|
xmlns:views="clr-namespace:Flawless.Client.Views"
|
||||||
x:DataType="vm:RepositoryViewModel"
|
x:DataType="vm:RepositoryViewModel"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Flawless.Client.Views.RepositoryPage.RepoSettingPageView">
|
x:Class="Flawless.Client.Views.RepositoryPage.RepoSettingPageView">
|
||||||
@ -56,8 +57,40 @@
|
|||||||
<Button HorizontalAlignment="Stretch" Classes="Danger" Content="Yes I Want to Delete this Repository"
|
<Button HorizontalAlignment="Stretch" Classes="Danger" Content="Yes I Want to Delete this Repository"
|
||||||
Command="{Binding DeleteRepositoryBothServerAndLocalCommand}"/>
|
Command="{Binding DeleteRepositoryBothServerAndLocalCommand}"/>
|
||||||
</u:FormItem>
|
</u:FormItem>
|
||||||
<u:FormItem Label="Webhook" IsVisible="{Binding ServerStatus.AllowWebHook}">
|
<u:FormItem Label="Webhook" IsVisible="{Binding ShowWebHook}">
|
||||||
|
<StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||||
|
<u:IconButton Content="Refresh" Icon="{StaticResource SemiIconRefresh}"
|
||||||
|
Command="{Binding RefreshWebhooksCommand}"/>
|
||||||
|
<u:IconButton Content="Add" Icon="{StaticResource SemiIconPlus}"
|
||||||
|
Command="{Binding AddWebhookCommand}"
|
||||||
|
IsVisible="{Binding IsOwnerRole}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<DataGrid ItemsSource="{Binding Repository.Webhooks}"
|
||||||
|
IsReadOnly="True"
|
||||||
|
AutoGenerateColumns="False">
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTextColumn Header="URL" Binding="{Binding TargetUrl}"/>
|
||||||
|
<DataGridCheckBoxColumn Header="Active" Binding="{Binding Active}"/>
|
||||||
|
<DataGridTextColumn Header="Type" Binding="{Binding EventType}"/>
|
||||||
|
<DataGridTemplateColumn IsVisible="{Binding IsOwnerRole}">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Button Content="Toggle Activate"
|
||||||
|
Command="{Binding $parent[views:RepositoryView].((vm:RepositoryViewModel)DataContext).ToggleWebhookCommand}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
<Button Content="Toggle Activate"
|
||||||
|
Command="{Binding $parent[views:RepositoryView].((vm:RepositoryViewModel)DataContext).DeleteWebhookCommand}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
</StackPanel>
|
||||||
</u:FormItem>
|
</u:FormItem>
|
||||||
</u:Form>
|
</u:Form>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user