mirror of
https://github.com/PGYER/codefever.git
synced 2026-05-31 06:00:16 +08:00
feat(Webhook): Support webhook (issues #24)
This commit is contained in:
@@ -40,6 +40,8 @@ class UserAccessController extends AccessController {
|
||||
const UAC_REPO_PROTECTED_BRANCH_RULE_CREATE = 0x1D;
|
||||
const UAC_REPO_PROTECTED_BRANCH_RULE_UPDATE = 0x1E;
|
||||
const UAC_REPO_PROTECTED_BRANCH_RULE_REOMVE = 0x1F;
|
||||
const UAC_REPO_WEBHOOK_EDIT = 0x22;
|
||||
const UAC_REPO_WEBHOOK_REMOVE = 0x23;
|
||||
|
||||
// role id
|
||||
const ROLE_NO_PERMISSION = 0;
|
||||
@@ -69,6 +71,7 @@ class UserAccessController extends AccessController {
|
||||
self::UAC_GROUP_CHANGE_MEMBER, self::UAC_GROUP_CREATE_REPO, self::UAC_GROUP_CHANGE_INFO,
|
||||
self::UAC_REPO_BRANCH_CREATE, self::UAC_REPO_BRANCH_REMOVE, self::UAC_REPO_TAG_CREATE, self::UAC_REPO_TAG_REMOVE, self::UAC_REPO_DEFAULT_BRANCH_CHANGE,
|
||||
self::UAC_REPO_PROTECTED_BRANCH_RULE_CREATE, self::UAC_REPO_PROTECTED_BRANCH_RULE_UPDATE, self::UAC_REPO_PROTECTED_BRANCH_RULE_REOMVE,
|
||||
self::UAC_REPO_WEBHOOK_EDIT, self::UAC_REPO_WEBHOOK_REMOVE,
|
||||
],
|
||||
self::ROLE_OWNER => [
|
||||
self::UAC_REPO_READ, self::UAC_REPO_PUSH, self::UAC_REPO_REMOVE, self::UAC_REPO_CHANGE_MEMBER, self::UAC_REPO_CHANGE_INFO, self::UAC_REPO_CHANGE_OWNER, self::UAC_REPO_CHANGE_URL,
|
||||
@@ -76,6 +79,7 @@ class UserAccessController extends AccessController {
|
||||
self::UAC_GROUP_REMOVE, self::UAC_GROUP_CHANGE_MEMBER, self::UAC_GROUP_CREATE_REPO, self::UAC_GROUP_CHANGE_INFO, self::UAC_GROUP_CHANGE_OWNER, self::UAC_GROUP_CHANGE_URL,
|
||||
self::UAC_REPO_BRANCH_CREATE, self::UAC_REPO_BRANCH_REMOVE, self::UAC_REPO_TAG_CREATE, self::UAC_REPO_TAG_REMOVE, self::UAC_REPO_DEFAULT_BRANCH_CHANGE,
|
||||
self::UAC_REPO_PROTECTED_BRANCH_RULE_CREATE, self::UAC_REPO_PROTECTED_BRANCH_RULE_UPDATE, self::UAC_REPO_PROTECTED_BRANCH_RULE_REOMVE,
|
||||
self::UAC_REPO_WEBHOOK_EDIT, self::UAC_REPO_WEBHOOK_REMOVE,
|
||||
],
|
||||
self::ROLE_NO_BODY => [] // no permission
|
||||
];
|
||||
|
||||
@@ -10,7 +10,8 @@ class GeneralEventDispatcher extends EventDispatcher {
|
||||
'*' => [
|
||||
['service\EventHandler\TestHandler@capture'],
|
||||
['service\EventHandler\ActivityHandler@capture'],
|
||||
['service\EventHandler\UserNotificationHandler@capture']
|
||||
['service\EventHandler\UserNotificationHandler@capture'],
|
||||
['service\EventHandler\WebhookHandler@capture'],
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
|
||||
$config['crontab'] = [
|
||||
// ['crontab', 'backend|customize', 'command']
|
||||
['* * * * *', 'backend', 'repository_webhook run'],
|
||||
];
|
||||
|
||||
@@ -1979,4 +1979,156 @@ class Repository extends Base
|
||||
|
||||
Response::output($result);
|
||||
}
|
||||
|
||||
public function getWebhook_post()
|
||||
{
|
||||
$uKey = Request::parse()->authData['userData']['u_key'];
|
||||
$data = Request::parse()->parsed;
|
||||
$rKey = $data['repository'];
|
||||
$rwKey = $data['rwKey'];
|
||||
|
||||
if (!$rKey || !$rwKey) {
|
||||
Response::reject(0x0201);
|
||||
}
|
||||
|
||||
if (!$this->service->requestRepositoryPermission($rKey, $uKey, UserAccessController::UAC_REPO_READ)) {
|
||||
Response::reject(0x0106);
|
||||
}
|
||||
|
||||
$webhook = $this->repositoryModel->getWebhook($rwKey);
|
||||
$webhook = $this->repositoryModel->normalizeWebhooks($webhook ? [$webhook] : []);
|
||||
Response::output($webhook ? $webhook[0] : []);
|
||||
}
|
||||
|
||||
public function webhooks_post()
|
||||
{
|
||||
$uKey = Request::parse()->authData['userData']['u_key'];
|
||||
$rKey = Request::parse()->parsed['repository'];
|
||||
|
||||
if (!$rKey) {
|
||||
Response::reject(0x0201);
|
||||
}
|
||||
|
||||
if (!$this->service->requestRepositoryPermission($rKey, $uKey, UserAccessController::UAC_REPO_READ)) {
|
||||
Response::reject(0x0106);
|
||||
}
|
||||
|
||||
$webhooks = $this->repositoryModel->getWebhooks($rKey);
|
||||
$webhooks = $this->repositoryModel->normalizeWebhooks($webhooks);
|
||||
|
||||
Response::output($webhooks);
|
||||
}
|
||||
|
||||
public function editWebhook_post()
|
||||
{
|
||||
$uKey = Request::parse()->authData['userData']['u_key'];
|
||||
$data = Request::parse()->parsed;
|
||||
$rKey = $data['repository'];
|
||||
$rwKey = $data['rwKey'];
|
||||
$url = $data['url'];
|
||||
$secret = $data['secret'];
|
||||
$events = $data['events'];
|
||||
$active = (int) $data['active'];
|
||||
|
||||
if (!$url || !$events || !in_array($active, [1, 2])) {
|
||||
Response::reject(0x0201);
|
||||
}
|
||||
|
||||
if (!$this->service->requestRepositoryPermission($rKey, $uKey, UserAccessController::UAC_REPO_WEBHOOK_EDIT)) {
|
||||
Response::reject(0x0106);
|
||||
}
|
||||
|
||||
$dbData = array(
|
||||
'rw_url' => $url,
|
||||
'rw_secret' => $secret,
|
||||
'rw_events' => $events,
|
||||
'rw_active' => $active
|
||||
);
|
||||
|
||||
if ($rwKey) {
|
||||
$result = $this->repositoryModel->updateWebhook($rwKey, $dbData);
|
||||
$eventType = 'WEBHOOK_UPDATE';
|
||||
} else {
|
||||
$dbData['u_key'] = $uKey;
|
||||
$dbData['r_key'] = $rKey;
|
||||
$result = $this->repositoryModel->createWebhook($dbData);
|
||||
$eventType = 'WEBHOOK_CREATE';
|
||||
}
|
||||
|
||||
if (!$result) {
|
||||
Response::reject(0x0405);
|
||||
}
|
||||
|
||||
$repository = $this->repositoryModel->get($rKey);
|
||||
$this->service->newEvent($eventType, [
|
||||
'gKey' => $repository['g_key'],
|
||||
'rKey' => $rKey,
|
||||
'rwKey' => $result
|
||||
], $uKey);
|
||||
|
||||
Response::output([]);
|
||||
}
|
||||
|
||||
public function deleteWebhook_post()
|
||||
{
|
||||
$uKey = Request::parse()->authData['userData']['u_key'];
|
||||
$data = Request::parse()->parsed;
|
||||
$rKey = $data['repository'];
|
||||
$rwKey = $data['rwKey'];
|
||||
|
||||
if (!$rKey || !$rwKey) {
|
||||
Response::reject(0x0201);
|
||||
}
|
||||
|
||||
if (!$this->service->requestRepositoryPermission($rKey, $uKey, UserAccessController::UAC_REPO_WEBHOOK_REMOVE)) {
|
||||
Response::reject(0x0106);
|
||||
}
|
||||
|
||||
if (!$this->repositoryModel->deleteWebhook($rwKey)) {
|
||||
Response::reject(0x0405);
|
||||
}
|
||||
|
||||
// DELETE EVENTS AND LOGS
|
||||
$this->repositoryModel->deleteWebhookEventsByRwKey($rwKey);
|
||||
$this->repositoryModel->deleteWebhookLogsByRwKey($rwKey);
|
||||
|
||||
$repository = $this->repositoryModel->get($rKey);
|
||||
$this->service->newEvent('WEBHOOK_DELETE', [
|
||||
'gKey' => $repository['g_key'],
|
||||
'rKey' => $rKey,
|
||||
'rwKey' => $rwKey,
|
||||
], $uKey);
|
||||
|
||||
Response::output([]);
|
||||
}
|
||||
|
||||
public function getRepositoryWebhookLogs_post()
|
||||
{
|
||||
$data = Request::parse()->parsed;
|
||||
$rwKey = $data['webhook'];
|
||||
|
||||
if (!$rwKey) {
|
||||
Response::reject(0x0201);
|
||||
}
|
||||
|
||||
$logs = $this->repositoryModel->getRepositoryWebhookLogs($rwKey);
|
||||
$logs = $this->repositoryModel->normalizeRepositoryWebhookLogs($logs);
|
||||
|
||||
Response::output($logs);
|
||||
}
|
||||
|
||||
public function getRepositoryWebhookLogData_post()
|
||||
{
|
||||
$data = Request::parse()->parsed;
|
||||
$id = $data['id'];
|
||||
|
||||
if (!$id) {
|
||||
Response::reject(0x0201);
|
||||
}
|
||||
|
||||
$log = $this->repositoryModel->getRepositoryWebhookLogData($id);
|
||||
$log = $this->repositoryModel->normalizeRepositoryWebhookLogData($log);
|
||||
|
||||
Response::output($log);
|
||||
}
|
||||
}
|
||||
|
||||
137
application/controllers/backend/repository_webhook.php
Normal file
137
application/controllers/backend/repository_webhook.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
set_time_limit(0);
|
||||
use service\Utility\UUID;
|
||||
use GuzzleHttp\Client;
|
||||
|
||||
class Repository_webhook extends CI_Controller {
|
||||
private $_eventDataMap = [
|
||||
'rKey' => 'repositoryId',
|
||||
'gKey' => 'groupId',
|
||||
'uid' => 'userId',
|
||||
'mrKey' => 'mergerequestId',
|
||||
'sourceRKey' => 'sourceRepositoryId',
|
||||
'sourceGKey' => 'sourceGroupId',
|
||||
'mrrKey' => 'reviewId',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->load->library('service');
|
||||
$this->load->model('Repository_model', 'repositoryModel');
|
||||
$this->load->model('Group_model', 'groupModel');
|
||||
$this->load->model('User_model', 'userModel');
|
||||
}
|
||||
|
||||
public function run()
|
||||
{
|
||||
$events = $this->repositoryModel->getRepositoryWebhookEvents();
|
||||
if (!$events) {
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$client = new Client();
|
||||
|
||||
foreach ($events as $event) {
|
||||
$repository = $this->repositoryModel->get($event['r_key']);
|
||||
if (!$repository) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$group = $this->groupModel->get($repository['g_key']);
|
||||
if (!$group) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$user = $this->userModel->get($event['rwe_user']);
|
||||
if (!$user) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$uuid = UUID::getUUID();
|
||||
|
||||
$body = json_encode([
|
||||
'event' => $event['rwe_type'],
|
||||
'data' => $this->_normalizeEventData(json_decode($event['rwe_data'], TRUE)),
|
||||
'repository' => [
|
||||
'id' => $repository['r_key'],
|
||||
'url' => YAML_HOST . '/' . $group['g_name'] . '/' . $repository['r_name'],
|
||||
],
|
||||
'sender' => [
|
||||
'id' => $user['u_key'],
|
||||
'name' => $user['u_name'],
|
||||
],
|
||||
]);
|
||||
|
||||
$headers = [
|
||||
'Request URL' => $event['rw_url'],
|
||||
'Request method' => 'POST',
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json',
|
||||
'User-Agent' => 'CodeFever-Webhook',
|
||||
'X-CodeFever-Id' => $uuid,
|
||||
'X-CodeFever-Event' => $event['rwe_type'],
|
||||
'X-CodeFever-Signature' => 'md5=' . $this->_signature($body, $event['rw_secret']),
|
||||
];
|
||||
|
||||
$start = microtime(TRUE);
|
||||
|
||||
try {
|
||||
$response = $client->request(
|
||||
'POST',
|
||||
$event['rw_url'],
|
||||
[
|
||||
'body' => $body,
|
||||
'headers' => $headers,
|
||||
'http_errors' => FALSE,
|
||||
'timeout' => 30,
|
||||
]
|
||||
);
|
||||
|
||||
$status = $response->getStatusCode();
|
||||
$responseHeaders = $response->getHeaders();
|
||||
$responseBody = (string) $response->getBody();
|
||||
} catch (Exception $e) {
|
||||
$status = 400;
|
||||
$responseHeaders = [];
|
||||
$responseBody = '';
|
||||
}
|
||||
|
||||
if ($status == 200) {
|
||||
$this->repositoryModel->deleteRepositoryWebhookEvent($event['rwe_key']);
|
||||
}
|
||||
|
||||
$this->repositoryModel->addRepositoryWebhookLog([
|
||||
'rwl_id' => $uuid,
|
||||
'rw_key' => $event['rw_key'],
|
||||
'rwl_request' => json_encode([
|
||||
'headers' => $headers,
|
||||
'body' => $body,
|
||||
]),
|
||||
'rwl_response' => json_encode([
|
||||
'headers' => $responseHeaders,
|
||||
'body' => $responseBody,
|
||||
]),
|
||||
'rwl_start' => $start,
|
||||
'rwl_end' => microtime(TRUE),
|
||||
'rwl_status' => $status,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function _signature(string $data, string $secret)
|
||||
{
|
||||
return md5($data . ($secret ? $secret : ''));
|
||||
}
|
||||
|
||||
private function _normalizeEventData(array $data)
|
||||
{
|
||||
$final = [];
|
||||
foreach ($data as $key => $val) {
|
||||
$final[isset($this->_eventDataMap[$key]) ? $this->_eventDataMap[$key] : $key] = $val;
|
||||
}
|
||||
|
||||
return $final;
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,10 @@ class ActivityHandler extends EventHandler
|
||||
EventType::MERGE_REQUEST_REVIEWER_DELETE,
|
||||
EventType::MERGE_REQUEST_REVIEWER_REVIEW,
|
||||
|
||||
EventType::WEBHOOK_CREATE,
|
||||
EventType::WEBHOOK_UPDATE,
|
||||
EventType::WEBHOOK_DELETE,
|
||||
|
||||
EventType::HOOK_POST_RECEIVE
|
||||
];
|
||||
|
||||
@@ -125,6 +129,12 @@ class ActivityHandler extends EventHandler
|
||||
EventType::MERGE_REQUEST_REVIEWER_REVIEW
|
||||
])) {
|
||||
$this->_handleMergeRequestEvent($event);
|
||||
} else if (in_array($eventType, [
|
||||
EventType::WEBHOOK_CREATE,
|
||||
EventType::WEBHOOK_UPDATE,
|
||||
EventType::WEBHOOK_DELETE
|
||||
])) {
|
||||
$this->_handleWebhookEvent($event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,4 +447,33 @@ class ActivityHandler extends EventHandler
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
private function _handleWebhookEvent(Event $event) {
|
||||
$eventActivityTypeMapping = [
|
||||
EventType::WEBHOOK_CREATE => ActivityType::WEBHOOK_CREATE,
|
||||
EventType::WEBHOOK_UPDATE => ActivityType::WEBHOOK_UPDATE,
|
||||
EventType::WEBHOOK_DELETE => ActivityType::WEBHOOK_DELETE
|
||||
];
|
||||
|
||||
$insertData = [];
|
||||
$insertData['a_key'] = UUID::getKey();
|
||||
$insertData['u_key'] = $event->user;
|
||||
$insertData['a_relative_r_key'] = $event->data->rKey;
|
||||
$insertData['a_relative_g_key'] = $event->data->gKey;
|
||||
|
||||
if (in_array($event->type, [
|
||||
EventType::WEBHOOK_CREATE,
|
||||
EventType::WEBHOOK_UPDATE,
|
||||
EventType::WEBHOOK_DELETE
|
||||
])) {
|
||||
$insertData['a_type'] = $eventActivityTypeMapping[$event->type];
|
||||
$insertData['a_data'] = json_encode([
|
||||
'rwKey' => $event->data->rwKey
|
||||
]);
|
||||
}
|
||||
|
||||
$this->CI->db->insert('activities', $insertData);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
62
application/event_handlers/WebhookHandler.php
Executable file
62
application/event_handlers/WebhookHandler.php
Executable file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace service\EventHandler;
|
||||
|
||||
use service\Event\Event;
|
||||
use service\Constant\EventType;
|
||||
|
||||
class WebhookHandler extends EventHandler
|
||||
{
|
||||
protected $ListenedEventList = [
|
||||
EventType::REPO_CREATE,
|
||||
EventType::REPO_FORK,
|
||||
EventType::REPO_UPDATE_AVATAR,
|
||||
EventType::REPO_UPDATE_NAME,
|
||||
EventType::REPO_UPDATE_DESCRIPTION,
|
||||
EventType::REPO_ADD_MEMBER,
|
||||
EventType::REPO_CHANGE_MEMBER_ROLE,
|
||||
EventType::REPO_REMOVE_MEMBER,
|
||||
EventType::REPO_CHANGE_OWNER,
|
||||
EventType::REPO_CHANGE_URL,
|
||||
EventType::REPO_REMOVE,
|
||||
|
||||
EventType::BRANCH_CREATE,
|
||||
EventType::BRANCH_REMOVE,
|
||||
EventType::DEFAULT_BRANCH_CHANGE,
|
||||
EventType::PROTECTED_BRANCH_RULE_CREATE,
|
||||
EventType::PROTECTED_BRANCH_RULE_CHANGE,
|
||||
EventType::PROTECTED_BRANCH_RULE_REMOVE,
|
||||
|
||||
EventType::TAG_CREATE,
|
||||
EventType::TAG_REMOVE,
|
||||
|
||||
EventType::MERGE_REQUEST_CREATE,
|
||||
EventType::MERGE_REQUEST_CLOSE,
|
||||
EventType::MERGE_REQUEST_MERGE,
|
||||
EventType::MERGE_REQUEST_REVIEWER_CREATE,
|
||||
EventType::MERGE_REQUEST_REVIEWER_DELETE,
|
||||
EventType::MERGE_REQUEST_REVIEWER_REVIEW,
|
||||
|
||||
EventType::HOOK_POST_RECEIVE
|
||||
];
|
||||
|
||||
function capture(Event $event)
|
||||
{
|
||||
$eventType = $event->type;
|
||||
if(in_array($eventType, $this->ListenedEventList)) {
|
||||
$this->_processEvent($event);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function _processEvent(Event $event)
|
||||
{
|
||||
$this->CI->load->model('Repository_model', 'repositoryModel');
|
||||
$this->CI->repositoryModel->addRepositoryWebhookEvent(
|
||||
$event->user,
|
||||
$event->data->rKey,
|
||||
$event->type,
|
||||
$event->data->getData()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -55,4 +55,9 @@ class ActivityType {
|
||||
const MERGE_REQUEST_REVIEWER_CREATE = 0x0704;
|
||||
const MERGE_REQUEST_REVIEWER_DELETE = 0x0705;
|
||||
const MERGE_REQUEST_REVIEWER_REVIEW = 0x0706;
|
||||
|
||||
// webhook
|
||||
const WEBHOOK_CREATE = 0x0901;
|
||||
const WEBHOOK_UPDATE = 0x0902;
|
||||
const WEBHOOK_DELETE = 0x0903;
|
||||
}
|
||||
|
||||
@@ -132,6 +132,16 @@ class EventType {
|
||||
const MERGE_REQUEST_REVIEWER_REVIEW = 'mergeRequestReviewer:review';
|
||||
const MERGE_REQUEST_REVIEWER_REVIEW_D = self::MERGE_REQUEST_REVIEWER_DELETE_D;
|
||||
|
||||
// webhook event
|
||||
const WEBHOOK_CREATE = 'webhook:create';
|
||||
const WEBHOOK_CREATE_D = 'gKey:string|rKey:string|rwKey:string';
|
||||
|
||||
const WEBHOOK_UPDATE = 'webhook:update';
|
||||
const WEBHOOK_UPDATE_D = 'gKey:string|rKey:string|rwKey:string';
|
||||
|
||||
const WEBHOOK_DELETE = 'webhook:delete';
|
||||
const WEBHOOK_DELETE_D = 'gKey:string|rKey:string|rwKey:string';
|
||||
|
||||
// repository hooks event
|
||||
const HOOK = 'hook';
|
||||
const HOOK_D = '';
|
||||
|
||||
@@ -2105,4 +2105,232 @@ class Repository_model extends CI_Model
|
||||
|
||||
return $members;
|
||||
}
|
||||
|
||||
public function normalizeWebhooks(array $list)
|
||||
{
|
||||
$output = [];
|
||||
if (!$list) {
|
||||
return [];
|
||||
}
|
||||
foreach ($list as $item) {
|
||||
array_push($output, [
|
||||
'id' => $item['rw_key'],
|
||||
'user' => $item['u_name'],
|
||||
'url' => $item['rw_url'],
|
||||
'secret' => $item['rw_secret'],
|
||||
'events' => $item['rw_events'],
|
||||
'active' => $item['rw_active'],
|
||||
'updated' => (int) strtotime($item['rw_updated'])
|
||||
]);
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function getWebhook(string $rwKey) {
|
||||
$this->db->where('rw_key', $rwKey);
|
||||
$query = $this->db->get('repository_webhooks');
|
||||
|
||||
return $query->row_array();
|
||||
}
|
||||
|
||||
public function getWebhooks (string $rKey) {
|
||||
$this->db->select('rw.*, u.u_name');
|
||||
$this->db->from('repository_webhooks AS rw');
|
||||
$this->db->join('users AS u', 'rw.u_key = u.u_key', 'left');
|
||||
$this->db->where('rw.r_key', $rKey);
|
||||
$query = $this->db->get();
|
||||
return $query->result_array();
|
||||
}
|
||||
|
||||
public function createWebhook (array $data)
|
||||
{
|
||||
if (!$data['u_key'] || !$data['r_key'] || !$data['rw_url'] || !$data['rw_events']) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$data['rw_key'] = UUID::getKey();
|
||||
$data['rw_updated'] = date('Y-m-d H:i:s');
|
||||
$this->db->insert('repository_webhooks', $data);
|
||||
|
||||
return $data['rw_key'];
|
||||
}
|
||||
|
||||
public function updateWebhook(string $rwKey, array $data)
|
||||
{
|
||||
if (!$rwKey || !$data['rw_url'] || !$data['rw_events']) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$data['rw_updated'] = date('Y-m-d H:i:s');
|
||||
|
||||
$this->db->where('rw_key', $rwKey);
|
||||
$this->db->update('repository_webhooks', $data);
|
||||
|
||||
return $rwKey;
|
||||
}
|
||||
|
||||
public function deleteWebhook (string $rwKey)
|
||||
{
|
||||
if (!$rwKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->db->where('rw_key', $rwKey);
|
||||
$this->db->delete('repository_webhooks');
|
||||
|
||||
return $rwKey;
|
||||
}
|
||||
|
||||
public function deleteWebhookEventsByRwKey (string $rwKey)
|
||||
{
|
||||
if (!$rwKey) {
|
||||
return false;
|
||||
}
|
||||
$this->db->where('rw_key', $rwKey);
|
||||
$this->db->delete('repository_webhook_events');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function deleteWebhookLogsByRwKey (string $rwKey)
|
||||
{
|
||||
if (!$rwKey) {
|
||||
return false;
|
||||
}
|
||||
$this->db->where('rw_key', $rwKey);
|
||||
$this->db->delete('repository_webhook_logs');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function addRepositoryWebhookEvent(string $uKey, string $rKey, string $eventType, string $data)
|
||||
{
|
||||
if (!$uKey || !$rKey || !$eventType || !$data) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$webhooks = $this->getRepositoryWebhooks($rKey);
|
||||
if (!$webhooks) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
foreach ($webhooks as $webhook) {
|
||||
$eventTypes = explode(',', $webhook['rw_events']);
|
||||
|
||||
if (!in_array($eventType, $eventTypes)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->db->insert('repository_webhook_events', [
|
||||
'rwe_key' => UUID::getKey(),
|
||||
'rwe_user' => $uKey,
|
||||
'rw_key' => $webhook['rw_key'],
|
||||
'rwe_type' => $eventType,
|
||||
'rwe_data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
public function getRepositoryWebhooks(string $rKey)
|
||||
{
|
||||
if (!$rKey) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$this->db->where('r_key', $rKey);
|
||||
$query = $this->db->get('repository_webhooks');
|
||||
$webhooks = $query->result_array();
|
||||
|
||||
return $webhooks;
|
||||
}
|
||||
|
||||
public function getRepositoryWebhookEvents()
|
||||
{
|
||||
$this->db->from('repository_webhook_events AS rwe');
|
||||
$this->db->join('repository_webhooks AS rw', 'rwe.rw_key = rw.rw_key', 'LEFT');
|
||||
$this->db->where('rw_active', GLOBAL_TRUE);
|
||||
$query = $this->db->get();
|
||||
$events = $query->result_array();
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
public function deleteRepositoryWebhookEvent(string $rweKey)
|
||||
{
|
||||
if (!$rweKey) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$this->db->where('rwe_key', $rweKey);
|
||||
$this->db->delete('repository_webhook_events');
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
public function addRepositoryWebhookLog(array $data)
|
||||
{
|
||||
if (!$data) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$this->db->insert('repository_webhook_logs', $data);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
public function normalizeRepositoryWebhookLogData(array $data)
|
||||
{
|
||||
return [
|
||||
'request' => json_decode($data['rwl_request']),
|
||||
'response' => json_decode($data['rwl_response']),
|
||||
];
|
||||
}
|
||||
|
||||
public function getRepositoryWebhookLogData(string $rwlId)
|
||||
{
|
||||
if (!$rwlId) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$this->db->where('rwl_id', $rwlId);
|
||||
$query = $this->db->get('repository_webhook_logs');
|
||||
$log = $query->row_array();
|
||||
|
||||
return $log;
|
||||
}
|
||||
|
||||
public function normalizeRepositoryWebhookLogs(array $list)
|
||||
{
|
||||
$final = [];
|
||||
foreach ($list as $item) {
|
||||
array_push($final, [
|
||||
'id' => $item['rwl_id'],
|
||||
'webhook' => $item['rw_key'],
|
||||
'start' => (float) $item['rwl_start'],
|
||||
'end' => (float) $item['rwl_end'],
|
||||
'status' => (int) $item['rwl_status'],
|
||||
'success' => $item['rwl_status'] == 200,
|
||||
'created' => $item['rwl_created'],
|
||||
]);
|
||||
}
|
||||
|
||||
return $final;
|
||||
}
|
||||
|
||||
public function getRepositoryWebhookLogs(string $rwKey)
|
||||
{
|
||||
if (!$rwKey) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$this->db->select('rwl_id, rw_key, rwl_start, rwl_end, rwl_status, rwl_created');
|
||||
$this->db->where('rw_key', $rwKey);
|
||||
$this->db->order_by('rwl_created', 'DESC');
|
||||
$query = $this->db->get('repository_webhook_logs');
|
||||
$logs = $query->result_array();
|
||||
|
||||
return $logs;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ GroupRepositoryMenu.propTypes = {
|
||||
currentRepositoryKey: PropTypes.string,
|
||||
currentGroupKey: PropTypes.string,
|
||||
intl: PropTypes.object.isRequired,
|
||||
type: PropTypes.string.isRequired
|
||||
type: PropTypes.string
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
|
||||
604
www/view/src/components/view/RepositorySettingWebhook.js
Executable file
604
www/view/src/components/view/RepositorySettingWebhook.js
Executable file
@@ -0,0 +1,604 @@
|
||||
// core
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { injectIntl } from 'react-intl'
|
||||
|
||||
// components
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import Button from '@material-ui/core/Button'
|
||||
import TextField from '@material-ui/core/TextField'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import CircularProgress from '@material-ui/core/CircularProgress'
|
||||
import Radio from '@material-ui/core/Radio'
|
||||
import RadioGroup from '@material-ui/core/RadioGroup'
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel'
|
||||
import Checkbox from '@material-ui/core/Checkbox'
|
||||
import { psLog, plEdit, plTrash } from '@pgyer/icons'
|
||||
import SquareIconButton from 'APPSRC/components/unit/SquareIconButton'
|
||||
import TableList from 'APPSRC/components/unit/TableList'
|
||||
import RepositoryData from 'APPSRC/data_providers/RepositoryData'
|
||||
import Events from 'APPSRC/config/WebhookEventConfig'
|
||||
import FormattedTime from 'APPSRC/components/unit/FormattedTime'
|
||||
import Tooltip from '@material-ui/core/Tooltip'
|
||||
import WebhookLog from 'APPSRC/components/view/unit/WebhookLog'
|
||||
import ShowHelper from 'APPSRC/components/unit/ShowHelper'
|
||||
|
||||
// helpers
|
||||
import NetworkHelper from 'APPSRC/helpers/NetworkHelper'
|
||||
import { copyToClipboard } from 'APPSRC/helpers/VaribleHelper'
|
||||
import ValidatorGenerator from 'APPSRC/helpers/ValidatorGenerator'
|
||||
import EventGenerator from 'APPSRC/helpers/EventGenerator'
|
||||
|
||||
const styles = (theme) => ({
|
||||
loading: {
|
||||
paddingTop: theme.spacing(16),
|
||||
paddingBottom: theme.spacing(16),
|
||||
justifyContent: 'center'
|
||||
},
|
||||
header: {
|
||||
display: 'flex',
|
||||
marginBottom: theme.spacing(4),
|
||||
justifyContent: 'space-between',
|
||||
lineHeight: theme.spacing(5) + 'px',
|
||||
borderBottom: '1px solid ' + theme.palette.border,
|
||||
fontSize: '18px'
|
||||
},
|
||||
webhookForm: {
|
||||
paddingTop: theme.spacing(6),
|
||||
marginBottom: theme.spacing(2),
|
||||
paddingBottom: theme.spacing(6)
|
||||
},
|
||||
btn: {
|
||||
verticalAlign: 'bottom',
|
||||
marginTop: theme.spacing(2),
|
||||
marginLeft: theme.spacing(3)
|
||||
},
|
||||
icon: {
|
||||
color: theme.palette.text.light
|
||||
},
|
||||
need: {
|
||||
color: 'red'
|
||||
},
|
||||
logs: {
|
||||
marginTop: theme.spacing(3)
|
||||
},
|
||||
dot: {
|
||||
width: theme.spacing(1),
|
||||
height: theme.spacing(1),
|
||||
borderRadius: '50%',
|
||||
backgroundColor: theme.palette.primary.main
|
||||
},
|
||||
close: {
|
||||
backgroundColor: theme.palette.error.main
|
||||
},
|
||||
cursorPointer: {
|
||||
cursor: 'pointer'
|
||||
}
|
||||
})
|
||||
|
||||
class RepositorySettingWebhook extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
pending: false,
|
||||
webhooks: null,
|
||||
webhook: null,
|
||||
pushEvent: 'hook:postReceive',
|
||||
|
||||
edit: false,
|
||||
webhookLogs: null,
|
||||
isShowWebhookForm: !!window.location.search,
|
||||
url: '',
|
||||
secret: '',
|
||||
trigger: '1',
|
||||
active: '1',
|
||||
error: {},
|
||||
events: JSON.parse(JSON.stringify(Events))
|
||||
}
|
||||
|
||||
this.checkInput = ValidatorGenerator.stateValidator(this, [
|
||||
{
|
||||
name: 'url',
|
||||
passPattern: /^.+$/,
|
||||
errorMessage: this.props.intl.formatMessage(
|
||||
{ id: 'message.error._S_empty' },
|
||||
{ s: this.props.intl.formatMessage({ id: 'label.url' }) }
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'url',
|
||||
passPattern: /^http(s)?:\/\/.+/,
|
||||
errorMessage: this.props.intl.formatMessage(
|
||||
{ id: 'message.error._S_invalid' },
|
||||
{ s: this.props.intl.formatMessage({ id: 'label.url' }) }
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'url',
|
||||
passPattern: /^\S{0,255}$/,
|
||||
errorMessage: this.props.intl.formatMessage(
|
||||
{ id: 'message.error.noMoreThan_N_characters' },
|
||||
{ n: 255 }
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'secret',
|
||||
passPattern: /^\S{0,255}$/,
|
||||
errorMessage: this.props.intl.formatMessage(
|
||||
{ id: 'message.error.noMoreThan_N_characters' },
|
||||
{ n: 255 }
|
||||
)
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.getData(this.props)
|
||||
}
|
||||
|
||||
shouldComponentUpdate (nextProps, nextState) {
|
||||
if (JSON.stringify(nextProps.currentRepositoryKey) !== JSON.stringify(this.props.currentRepositoryKey)) {
|
||||
this.getData(nextProps)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
getData (props) {
|
||||
const { currentRepositoryKey } = props
|
||||
if (!currentRepositoryKey) {
|
||||
return false
|
||||
}
|
||||
|
||||
this.setState({ pending: true })
|
||||
RepositoryData.webhooks({
|
||||
repository: currentRepositoryKey
|
||||
}).then(NetworkHelper.withEventdispatcher(this.props.dispatchEvent)(NetworkHelper.getJSONData))
|
||||
.then(data => {
|
||||
if (!data.code) {
|
||||
const webhooks = data.data
|
||||
webhooks.map((item, index) => {
|
||||
item.events = this.getEvents(item.events)
|
||||
return true
|
||||
})
|
||||
this.setState({
|
||||
pending: false,
|
||||
webhooks: webhooks
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getWebhookLogs (rwKey) {
|
||||
if (!rwKey) {
|
||||
return false
|
||||
}
|
||||
|
||||
RepositoryData.getRepositoryWebhookLogs({ webhook: rwKey })
|
||||
.then(NetworkHelper.withEventdispatcher(this.props.dispatchEvent)(NetworkHelper.getJSONData))
|
||||
.then(data => {
|
||||
if (!data.code) {
|
||||
this.setState({ webhookLogs: data.data })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
editWebhook () {
|
||||
const { intl, currentRepositoryKey } = this.props
|
||||
const { pending, isShowWebhookForm, trigger, url, secret, webhook, edit, active } = this.state
|
||||
if (pending || !isShowWebhookForm || !this.checkInput()) {
|
||||
return false
|
||||
}
|
||||
|
||||
const events = trigger === '1' ? this.state.pushEvent : this.getCheckedEvents()
|
||||
if (!events) {
|
||||
this.props.dispatchEvent(EventGenerator.NewNotification(
|
||||
intl.formatMessage({ id: 'message.webhookEventsNeed' })
|
||||
, 1)
|
||||
)
|
||||
}
|
||||
|
||||
this.setState({ pending: true })
|
||||
RepositoryData.editWebhook({
|
||||
repository: currentRepositoryKey,
|
||||
rwKey: webhook ? webhook.id : '',
|
||||
url: url,
|
||||
secret: secret,
|
||||
events: events,
|
||||
active: active
|
||||
}).then(NetworkHelper.withEventdispatcher(this.props.dispatchEvent)(NetworkHelper.getJSONData))
|
||||
.then(data => {
|
||||
this.setState({ pending: false })
|
||||
if (!data.code) {
|
||||
this.props.dispatchEvent(EventGenerator.NewNotification(
|
||||
intl.formatMessage({ id: edit ? 'message.updated' : 'message.created' })
|
||||
, 0)
|
||||
)
|
||||
this.initData()
|
||||
this.setState({ isShowWebhookForm: false })
|
||||
this.getData(this.props)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
updateWebhook (webhook) {
|
||||
const { currentRepositoryKey } = this.props
|
||||
|
||||
if (!currentRepositoryKey) {
|
||||
return false
|
||||
}
|
||||
|
||||
this.setState({ pending: true })
|
||||
RepositoryData.getWebhook({
|
||||
repository: currentRepositoryKey,
|
||||
rwKey: webhook.id
|
||||
}).then(NetworkHelper.withEventdispatcher(this.props.dispatchEvent)(NetworkHelper.getJSONData))
|
||||
.then(data => {
|
||||
if (!data.code) {
|
||||
const webhook = data.data
|
||||
this.setState({
|
||||
pending: false,
|
||||
edit: true,
|
||||
webhook: webhook,
|
||||
isShowWebhookForm: true,
|
||||
url: webhook.url,
|
||||
secret: webhook.secret,
|
||||
trigger: webhook.events === this.state.pushEvent ? '1' : '2',
|
||||
events: this.getEvents(webhook.events),
|
||||
active: webhook.active
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
deleteWebhook (webhook) {
|
||||
const { currentRepositoryKey, intl } = this.props
|
||||
this.props.dispatchEvent(EventGenerator.addComformation('delete_webhook', {
|
||||
title: intl.formatMessage(
|
||||
{ id: 'message.confirmDelete' },
|
||||
{ s: intl.formatMessage({ id: 'label.webhook' }) }),
|
||||
description: '',
|
||||
reject: () => { return true },
|
||||
accept: () => {
|
||||
RepositoryData.deleteWebhook({
|
||||
repository: currentRepositoryKey,
|
||||
rwKey: webhook.id
|
||||
}).then(NetworkHelper.withEventdispatcher(this.props.dispatchEvent)(NetworkHelper.getJSONData))
|
||||
.then(data => {
|
||||
this.props.dispatchEvent(EventGenerator.cancelComformation())
|
||||
if (!data.code) {
|
||||
this.props.dispatchEvent(EventGenerator.NewNotification(
|
||||
this.props.intl.formatMessage({ id: 'message.deleted' })
|
||||
, 0)
|
||||
)
|
||||
this.getData(this.props)
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
getTableData () {
|
||||
const { classes, intl } = this.props
|
||||
const { webhooks } = this.state
|
||||
const final = []
|
||||
webhooks.map((item) => {
|
||||
let eventCount = 0
|
||||
final.push([
|
||||
<Tooltip title={intl.formatMessage({ id: item.active === '1' ? 'label.enable' : 'label.disable' })} placement='top'>
|
||||
<div className={[classes.dot, classes.cursorPointer, item.active === '1' ? '' : classes.close].join(' ')}></div>
|
||||
</Tooltip>,
|
||||
<Typography variant='body1' component='div'>{item.user}</Typography>,
|
||||
<Tooltip title={item.url} placement='top'>
|
||||
<Typography
|
||||
variant='body1'
|
||||
component='div'
|
||||
className={classes.cursorPointer}
|
||||
onClick={e => copyToClipboard(item.url, () => this.props.dispatchEvent(EventGenerator.NewNotification(intl.formatMessage({ id: 'label.copied' }), 0)))}
|
||||
>
|
||||
{item.url.substr(0, 40)}
|
||||
</Typography>
|
||||
</Tooltip>,
|
||||
<Tooltip title={item.secret} placement='top'>
|
||||
<Typography
|
||||
variant='body1'
|
||||
component='div'
|
||||
className={classes.cursorPointer}
|
||||
onClick={e => copyToClipboard(item.secret, () => this.props.dispatchEvent(EventGenerator.NewNotification(intl.formatMessage({ id: 'label.copied' }), 0)))}
|
||||
>
|
||||
{item.secret.substr(0, 20)}
|
||||
</Typography>
|
||||
</Tooltip>,
|
||||
<Typography variant='body1' component='div'>
|
||||
{item.events.map((item, index) => {
|
||||
if (item.checked) {
|
||||
eventCount++
|
||||
if (eventCount === 4) {
|
||||
return '...'
|
||||
} else if (eventCount > 4) {
|
||||
return ''
|
||||
} else {
|
||||
return (typeof item.title === 'string'
|
||||
? intl.formatMessage({ id: item.title })
|
||||
: intl.formatMessage(
|
||||
{ id: item.title[0] },
|
||||
{ s: intl.formatMessage({ id: item.title[1] }) }
|
||||
)) + '; '
|
||||
}
|
||||
}
|
||||
return ''
|
||||
})}
|
||||
</Typography>,
|
||||
<Typography variant='body1' component='div'><FormattedTime timestamp={item.updated * 1} /></Typography>,
|
||||
<Typography>
|
||||
<SquareIconButton label='label.update' icon={plEdit} className={classes.icon} onClick={e => this.updateWebhook(item)} />
|
||||
<SquareIconButton label='label.delete' icon={plTrash} className={classes.icon} onClick={e => this.deleteWebhook(item)} />
|
||||
<SquareIconButton label='label.log' icon={psLog} className={classes.icon} onClick={e => this.getWebhookLogs(item.id)} />
|
||||
</Typography>
|
||||
])
|
||||
return true
|
||||
})
|
||||
|
||||
return [
|
||||
['10px', 'auto', 'auto', 'auto', 'auto', 'auto', 'auto'],
|
||||
['', 'label.creator', 'label.url', 'label.webhookSecret', 'label.webhookTrigger', 'label.updateTime', ''],
|
||||
...final
|
||||
]
|
||||
}
|
||||
|
||||
initData () {
|
||||
this.setState({
|
||||
edit: false,
|
||||
webhook: null,
|
||||
url: '',
|
||||
secret: '',
|
||||
trigger: '1',
|
||||
events: JSON.parse(JSON.stringify(Events)),
|
||||
active: '1'
|
||||
})
|
||||
}
|
||||
|
||||
getEvents (events) {
|
||||
const tmpEvents = JSON.parse(JSON.stringify(Events))
|
||||
events = events.split(',')
|
||||
tmpEvents.map((item) => {
|
||||
if (events.indexOf(item.event) > -1) {
|
||||
item.checked = true
|
||||
} else {
|
||||
item.checked = false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return tmpEvents
|
||||
}
|
||||
|
||||
changeEvent (e) {
|
||||
const { events } = this.state
|
||||
let checked = false
|
||||
if (e.target.checked) {
|
||||
checked = true
|
||||
}
|
||||
|
||||
events.map((item) => {
|
||||
if (item.event === e.target.value) {
|
||||
item.checked = checked
|
||||
}
|
||||
return true
|
||||
})
|
||||
this.setState({
|
||||
events: events
|
||||
})
|
||||
}
|
||||
|
||||
getCheckedEvents () {
|
||||
const { events } = this.state
|
||||
const checkedEvents = []
|
||||
events.map((item) => {
|
||||
if (item.checked) {
|
||||
checkedEvents.push(item.event)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return checkedEvents.join(',')
|
||||
}
|
||||
|
||||
render () {
|
||||
const { classes, intl } = this.props
|
||||
const { pending, webhooks, webhookLogs, isShowWebhookForm, url, secret, trigger, events, edit, active, error } = this.state
|
||||
|
||||
return (<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant='h6' component='div' className={classes.header}>
|
||||
{intl.formatMessage({ id: 'label.webhookSetting' })}
|
||||
{!isShowWebhookForm && <Button
|
||||
color='primary'
|
||||
disableElevation
|
||||
variant='contained'
|
||||
disabled={pending}
|
||||
onClick={e => this.setState({ isShowWebhookForm: true })}
|
||||
>
|
||||
{intl.formatMessage({ id: 'label.createWebhook' })}
|
||||
</Button>}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
{isShowWebhookForm && <React.Fragment>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant='h6' component='div'>{intl.formatMessage({ id: edit ? 'label.updateWebhook' : 'label.createWebhook' })}</Typography>
|
||||
</Grid>
|
||||
<Grid container className={classes.webhookForm}>
|
||||
<Grid item xs={3} />
|
||||
<Grid item xs={6}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant='subtitle1' component='div'>{intl.formatMessage({ id: 'label.url' })} <span className={classes.need}>*</span></Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
variant='outlined'
|
||||
placeholder={intl.formatMessage({ id: 'message.error._S_empty' }, { s: intl.formatMessage({ id: 'label.url' }) })}
|
||||
value={url}
|
||||
error={!!error.url}
|
||||
helperText={error.url}
|
||||
onChange={e => this.setState({ url: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant='subtitle1' component='div'>{intl.formatMessage({ id: 'label.contentType' })}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant='body1' component='div'>application/json</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant='subtitle1' component='div'>
|
||||
{intl.formatMessage({ id: 'label.webhookSecret' })}
|
||||
|
||||
<ShowHelper type='icon' doc='/repo/webhooks.md' />
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
variant='outlined'
|
||||
placeholder={intl.formatMessage({ id: 'message.error._S_empty' }, { s: intl.formatMessage({ id: 'label.webhookSecret' }) })}
|
||||
value={secret}
|
||||
error={!!error.secret}
|
||||
helperText={error.secret}
|
||||
onChange={e => this.setState({ secret: e.target.value })}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant='subtitle1' component='div'>{intl.formatMessage({ id: 'label.webhookTrigger' })}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<RadioGroup value={trigger} onChange={e => this.setState({ trigger: e.target.value })}>
|
||||
<FormControlLabel value="1" control={<Radio />} label={intl.formatMessage({ id: 'label.pushTrigger' })} />
|
||||
<FormControlLabel value="2" control={<Radio />} label={
|
||||
<React.Fragment>
|
||||
<Typography variant='body1' component='span'>{intl.formatMessage({ id: 'label.customeTrigger' })}</Typography>
|
||||
|
||||
<ShowHelper type='icon' doc='/repo/webhooks.md' />
|
||||
</React.Fragment>} />
|
||||
</RadioGroup>
|
||||
</Grid>
|
||||
{trigger === '2' && <Grid item xs={12}>
|
||||
{events.map((item, index) => {
|
||||
return <FormControlLabel
|
||||
control={<Checkbox checked={item.checked} onChange={e => this.changeEvent(e)} value={item.event} />}
|
||||
label={
|
||||
typeof item.title === 'string'
|
||||
? intl.formatMessage({ id: item.title })
|
||||
: intl.formatMessage(
|
||||
{ id: item.title[0] },
|
||||
{ s: intl.formatMessage({ id: item.title[1] }) }
|
||||
)
|
||||
}
|
||||
/>
|
||||
})}
|
||||
</Grid>}
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Typography variant='subtitle1' component='div'>{intl.formatMessage({ id: 'label.status' })}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<RadioGroup value={active} onChange={e => { this.setState({ active: e.target.value }) }} row>
|
||||
<FormControlLabel value='1' control={<Radio />} label={intl.formatMessage({ id: 'label.enable' })} />
|
||||
<FormControlLabel value='2' control={<Radio />} label={intl.formatMessage({ id: 'label.disable' })} />
|
||||
</RadioGroup>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} align='right'>
|
||||
<Button
|
||||
color='primary'
|
||||
variant='outlined'
|
||||
disableElevation
|
||||
disabled={pending}
|
||||
onClick={e => {
|
||||
edit && this.initData()
|
||||
this.setState({ isShowWebhookForm: false })
|
||||
}}
|
||||
>
|
||||
{intl.formatMessage({ id: 'label.cancel' })}
|
||||
</Button>
|
||||
<Button
|
||||
color='primary'
|
||||
variant='contained'
|
||||
disableElevation
|
||||
className={classes.btn}
|
||||
disabled={pending}
|
||||
onClick={e => this.editWebhook()}
|
||||
>
|
||||
{pending && <CircularProgress size='1rem' color='inherit' />}
|
||||
{intl.formatMessage({ id: edit ? 'label.update' : 'label.create' })}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
}
|
||||
|
||||
{!isShowWebhookForm && <React.Fragment>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant='h6' component='div'>{intl.formatMessage({ id: 'label.webhookList' })}</Typography>
|
||||
</Grid>
|
||||
{webhooks
|
||||
? webhooks.length > 0
|
||||
? <Grid item xs={12}>
|
||||
<TableList data={this.getTableData()} />
|
||||
</Grid>
|
||||
: <Grid container spacing={2} className={classes.loading}>
|
||||
<Typography variant='body2' component='div'>{intl.formatMessage({ id: 'message.webhookListEmpty' })}</Typography>
|
||||
</Grid>
|
||||
: <Grid container spacing={2} className={classes.loading}>
|
||||
<CircularProgress />
|
||||
</Grid>
|
||||
}
|
||||
</Grid>
|
||||
{
|
||||
webhookLogs && <Grid container spacing={2} className={classes.logs}>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant='h5' component='div'>
|
||||
{intl.formatMessage({ id: 'label.webhookLog' })}
|
||||
<Typography variant='body2' component='span'>({intl.formatMessage({ id: 'message.show_n_record' }, { n: 30 })})</Typography>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<WebhookLog list={webhookLogs} />
|
||||
</Grid>
|
||||
}
|
||||
</React.Fragment>}
|
||||
</Grid>)
|
||||
}
|
||||
}
|
||||
|
||||
RepositorySettingWebhook.propTypes = {
|
||||
currentRepositoryKey: PropTypes.string.isRequired,
|
||||
dispatchEvent: PropTypes.func.isRequired,
|
||||
classes: PropTypes.object.isRequired,
|
||||
intl: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
currentRepositoryKey: state.DataStore.currentRepositoryKey
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch, ownProps) => {
|
||||
return {
|
||||
dispatchEvent: (event) => { dispatch(event) }
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(
|
||||
withStyles(styles)(
|
||||
connect(mapStateToProps, mapDispatchToProps)(RepositorySettingWebhook)
|
||||
)
|
||||
)
|
||||
204
www/view/src/components/view/unit/WebhookLog.js
Executable file
204
www/view/src/components/view/unit/WebhookLog.js
Executable file
@@ -0,0 +1,204 @@
|
||||
// core
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { injectIntl } from 'react-intl'
|
||||
|
||||
// components
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { plCopy, plClock, plClose, psConfirm, psError, psMore } from '@pgyer/icons'
|
||||
import SquareIconButton from 'APPSRC/components/unit/SquareIconButton'
|
||||
import InlineMarker from 'APPSRC/components/unit/InlineMarker'
|
||||
import TabHeader from 'APPSRC/components/unit/TabHeader'
|
||||
import TitleList from 'APPSRC/components/unit/TitleList'
|
||||
import RepositoryData from 'APPSRC/data_providers/RepositoryData'
|
||||
import ShowHelper from 'APPSRC/components/unit/ShowHelper'
|
||||
|
||||
// helpers
|
||||
import NetworkHelper from 'APPSRC/helpers/NetworkHelper'
|
||||
import { copyToClipboard } from 'APPSRC/helpers/VaribleHelper'
|
||||
|
||||
// style
|
||||
const styles = theme => ({
|
||||
webhook: {
|
||||
borderTop: '1px solid ' + theme.palette.border
|
||||
},
|
||||
subline: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: theme.spacing(6),
|
||||
padding: '0px ' + theme.spacing(3) + 'px'
|
||||
},
|
||||
date: {
|
||||
justifyContent: 'flex-end'
|
||||
},
|
||||
success: {
|
||||
color: theme.palette.success.main
|
||||
},
|
||||
error: {
|
||||
color: theme.palette.error.main
|
||||
},
|
||||
webhookid: {
|
||||
marginLeft: theme.spacing(2),
|
||||
marginRight: theme.spacing(1),
|
||||
borderRadius: theme.spacing(0.5),
|
||||
background: theme.palette.background.dark,
|
||||
padding: theme.spacing(0.5) + 'px ' + theme.spacing(1) + 'px'
|
||||
},
|
||||
more: {
|
||||
marginLeft: theme.spacing(2)
|
||||
},
|
||||
detail: {
|
||||
padding: theme.spacing(3),
|
||||
paddingTop: 0
|
||||
},
|
||||
time: {
|
||||
lineHeight: theme.spacing(5) + 'px'
|
||||
},
|
||||
code: {
|
||||
overflowX: 'auto',
|
||||
padding: theme.spacing(1),
|
||||
borderRadius: theme.spacing(0.5),
|
||||
background: theme.palette.background.main,
|
||||
border: '1px solid ' + theme.palette.border
|
||||
}
|
||||
})
|
||||
|
||||
class BranchList extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
webhookTab: 0,
|
||||
webhookId: '',
|
||||
logData: null
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
}
|
||||
|
||||
getData (id) {
|
||||
if (!id) {
|
||||
return false
|
||||
}
|
||||
|
||||
this.setState({ webhookTab: 0, webhookId: id, logData: null })
|
||||
RepositoryData.getRepositoryWebhookLogData({ id: id })
|
||||
.then(NetworkHelper.withEventdispatcher(this.props.dispatchEvent)(NetworkHelper.getJSONData))
|
||||
.then((data) => {
|
||||
if (!data.code) {
|
||||
this.setState({ logData: data.data })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getTime (start, end) {
|
||||
return Math.floor((end - start) * 100) / 100
|
||||
}
|
||||
|
||||
render () {
|
||||
const { list, classes, intl } = this.props
|
||||
const { webhookTab, webhookId, logData } = this.state
|
||||
|
||||
return <Grid item xs={12}>
|
||||
<TitleList title=''>
|
||||
{
|
||||
list.map(item => <Grid container key={item.id} className={classes.webhook}>
|
||||
<Grid item xs={8} className={classes.subline}>
|
||||
<FontAwesomeIcon icon={item.success ? psConfirm : psError} className={item.success ? classes.success : classes.error} />
|
||||
<Typography variant='body1' component='span' className={classes.webhookid}>{item.id}</Typography>
|
||||
<SquareIconButton label='label.copy' onClick={e => copyToClipboard(item.id)} icon={plCopy} />
|
||||
</Grid>
|
||||
<Grid item xs={4} className={[classes.subline, classes.date].join(' ')}>
|
||||
<Typography variant='body1' component='span'>{item.created}</Typography>
|
||||
{
|
||||
webhookId === item.id
|
||||
? <SquareIconButton label='label.close' onClick={e => this.setState({ webhookId: '' })} icon={plClose} className={classes.more} />
|
||||
: <SquareIconButton label='label.detail' onClick={e => this.getData(item.id)} icon={psMore} className={classes.more} />
|
||||
}
|
||||
</Grid>
|
||||
{
|
||||
webhookId && webhookId === item.id && logData && <Grid item xs={12} className={classes.detail}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<TabHeader
|
||||
tabs={[intl.formatMessage({ id: 'label.request' }), <Grid>{intl.formatMessage({ id: 'label.response' })} <InlineMarker color={item.success ? 'success' : 'error'} text={item.status + ''} /></Grid>]}
|
||||
currentTab={webhookTab}
|
||||
onChange={(e, newValue) => this.setState({ webhookTab: newValue })}
|
||||
>
|
||||
<Typography variant='body1' component='div' className={classes.time}>
|
||||
<FontAwesomeIcon icon={plClock} />
|
||||
{intl.formatMessage({ id: 'message.useTime_n' }, { n: this.getTime(item.start, item.end) })}
|
||||
</Typography>
|
||||
</TabHeader>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant='h5' component='div'>{intl.formatMessage({ id: 'label.httpHeaders' })}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Grid className={classes.code}>
|
||||
{
|
||||
webhookTab === 0
|
||||
? Object.keys(logData.request.headers).map(key => <Grid key={key}>
|
||||
<Typography variant='subtitle1' component='span'>{key}:</Typography> 
|
||||
<Typography variant='body1' component='span'>{logData.request.headers[key]}</Typography>
|
||||
</Grid>)
|
||||
: Object.keys(logData.response.headers).map(key => <Grid key={key}>
|
||||
<Typography variant='subtitle1' component='span'>{key}:</Typography> 
|
||||
<Typography variant='body1' component='span'>{logData.response.headers[key]}</Typography>
|
||||
</Grid>)
|
||||
}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant='h5' component='div'>
|
||||
{webhookTab === 0 ? intl.formatMessage({ id: 'label.httpPayload' }) : intl.formatMessage({ id: 'label.httpBody' })}
|
||||
|
||||
{webhookTab === 0 && <ShowHelper type='icon' doc='/repo/webhooks.md' />}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Grid className={classes.code}>
|
||||
<pre>
|
||||
<Typography variant='body1' component='div'>
|
||||
{webhookTab === 0 ? JSON.stringify(JSON.parse(logData.request.body), null, 4) : logData.response.body}
|
||||
</Typography>
|
||||
</pre>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
}
|
||||
</Grid>)
|
||||
}
|
||||
</TitleList>
|
||||
</Grid>
|
||||
}
|
||||
}
|
||||
|
||||
BranchList.propTypes = {
|
||||
list: PropTypes.array.isRequired,
|
||||
dispatchEvent: PropTypes.func.isRequired,
|
||||
classes: PropTypes.object.isRequired,
|
||||
intl: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch, ownProps) => {
|
||||
return {
|
||||
dispatchEvent: (event) => { dispatch(event) }
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(
|
||||
withStyles(styles)(
|
||||
connect(mapStateToProps, mapDispatchToProps)(BranchList)
|
||||
)
|
||||
)
|
||||
@@ -637,6 +637,39 @@ function parser (config) {
|
||||
),
|
||||
detail: config.formatter({ id: 'message.review_M_Reviewer' }, { m: mergeRequestLink })
|
||||
}
|
||||
} else if (code === 0x0901) {
|
||||
return {
|
||||
user,
|
||||
action: config.formatter(
|
||||
{ id: 'message.activity.create_S_Webhook' },
|
||||
{ s: config.relatedGroup.displayName + '/' + config.relatedRepository.displayName }
|
||||
),
|
||||
detail: <React.Fragment>
|
||||
{config.formatter({ id: 'label.createWebhook' })}
|
||||
</React.Fragment>
|
||||
}
|
||||
} else if (code === 0x0902) {
|
||||
return {
|
||||
user,
|
||||
action: config.formatter(
|
||||
{ id: 'message.activity.update_S_Webhook' },
|
||||
{ s: config.relatedGroup.displayName + '/' + config.relatedRepository.displayName }
|
||||
),
|
||||
detail: <React.Fragment>
|
||||
{config.formatter({ id: 'label.updateWebhook' })}
|
||||
</React.Fragment>
|
||||
}
|
||||
} else if (code === 0x0903) {
|
||||
return {
|
||||
user,
|
||||
action: config.formatter(
|
||||
{ id: 'message.activity.delete_S_Webhook' },
|
||||
{ s: config.relatedGroup.displayName + '/' + config.relatedRepository.displayName }
|
||||
),
|
||||
detail: <React.Fragment>
|
||||
{config.formatter({ id: 'label.deleteWebhook' })}
|
||||
</React.Fragment>
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -267,6 +267,14 @@ function makeRepositoryDrawerConfig (repositoryConfig) {
|
||||
/([A-Za-z0-9_]{5,})\/[A-Za-z0-9_]+\/settings\/branch(\/)?$/i
|
||||
]
|
||||
},
|
||||
{
|
||||
path: ['', repositoryConfig.group.name, repositoryConfig.repository.name, 'settings', 'webhook'].join('/'),
|
||||
name: 'menu.webhook_pl',
|
||||
icon: psSetting,
|
||||
activePattern: [
|
||||
/([A-Za-z0-9_]{5,})\/[A-Za-z0-9_]+\/settings\/webhook(\/)?$/i
|
||||
]
|
||||
},
|
||||
{
|
||||
path: ['', repositoryConfig.group.name, repositoryConfig.repository.name, 'settings', 'advanced'].join('/'),
|
||||
name: 'menu.advanced',
|
||||
|
||||
129
www/view/src/config/WebhookEventConfig.js
Executable file
129
www/view/src/config/WebhookEventConfig.js
Executable file
@@ -0,0 +1,129 @@
|
||||
const Events = [
|
||||
{
|
||||
event: 'hook:postReceive',
|
||||
title: 'label.pushEvent',
|
||||
checked: true
|
||||
},
|
||||
{
|
||||
event: 'repo:fork',
|
||||
title: 'label.forkRepository',
|
||||
checked: false
|
||||
},
|
||||
{
|
||||
event: 'repo:updateAvator',
|
||||
title: ['label.update_S_', 'label.repositoryAvatar'],
|
||||
checked: false
|
||||
},
|
||||
{
|
||||
event: 'repo:updateName',
|
||||
title: ['label.update_S_', 'label.repositoryName'],
|
||||
checked: false
|
||||
},
|
||||
{
|
||||
event: 'repo:updateDescription',
|
||||
title: ['label.update_S_', 'label.repositoryDescription'],
|
||||
checked: false
|
||||
},
|
||||
{
|
||||
event: 'repo:addMember',
|
||||
title: 'label.inviteMember',
|
||||
checked: false
|
||||
},
|
||||
{
|
||||
event: 'repo:changeMemberRole',
|
||||
title: 'label.changeMemberRole',
|
||||
checked: false
|
||||
},
|
||||
{
|
||||
event: 'repo:removeMember',
|
||||
title: 'label.removeMember',
|
||||
checked: false
|
||||
},
|
||||
{
|
||||
event: 'repo:changeOwner',
|
||||
title: ['label.update_S_', 'label.owner'],
|
||||
checked: false
|
||||
},
|
||||
{
|
||||
event: 'repo:changeURL',
|
||||
title: ['label.update_S_', 'label.repositoryURL'],
|
||||
checked: false
|
||||
},
|
||||
{
|
||||
event: 'repo:remove',
|
||||
title: 'label.deleteRepository',
|
||||
checked: false
|
||||
},
|
||||
{
|
||||
event: 'branch:create',
|
||||
title: 'label.newBranch',
|
||||
checked: false
|
||||
},
|
||||
{
|
||||
event: 'branch:remove',
|
||||
title: 'label.deleteBranch',
|
||||
checked: false
|
||||
},
|
||||
{
|
||||
event: 'branch:changeDefaultBranch',
|
||||
title: ['label.update_S_', 'label.defaultBranch'],
|
||||
checked: false
|
||||
},
|
||||
{
|
||||
event: 'branch:createProtectedBranchRule',
|
||||
title: 'label.createProtectedBranchRule',
|
||||
checked: false
|
||||
},
|
||||
{
|
||||
event: 'branch:changeProtectedBranchRule',
|
||||
title: 'label.changeProtectedBranchRule',
|
||||
checked: false
|
||||
},
|
||||
{
|
||||
event: 'branch:removeProtectedBranchRule',
|
||||
title: 'label.removeProtectedBranchRule',
|
||||
checked: false
|
||||
},
|
||||
{
|
||||
event: 'tag:create',
|
||||
title: 'label.newTag',
|
||||
checked: false
|
||||
},
|
||||
{
|
||||
event: 'tag:remove',
|
||||
title: 'label.deleteTag',
|
||||
checked: false
|
||||
},
|
||||
{
|
||||
event: 'mergeRequest:create',
|
||||
title: 'label.createMergeRequest',
|
||||
checked: false
|
||||
},
|
||||
{
|
||||
event: 'mergeRequest:close',
|
||||
title: 'label.closeMergeRequest',
|
||||
checked: false
|
||||
},
|
||||
{
|
||||
event: 'mergeRequest:merge',
|
||||
title: 'label.mergeRequest',
|
||||
checked: false
|
||||
},
|
||||
{
|
||||
event: 'mergeRequestReviewer:create',
|
||||
title: 'message.selectReviewer',
|
||||
checked: false
|
||||
},
|
||||
{
|
||||
event: 'mergeRequestReviewer:delete',
|
||||
title: 'message.deleteReviewer',
|
||||
checked: false
|
||||
},
|
||||
{
|
||||
event: 'mergeRequestReviewer:review',
|
||||
title: 'label.reviewReviewer',
|
||||
checked: false
|
||||
}
|
||||
]
|
||||
|
||||
export default Events
|
||||
@@ -184,6 +184,30 @@ function relatedMergeRequests (data) {
|
||||
return APIRequest.GET('/api/repository/relatedMergeRequests', null, data)
|
||||
}
|
||||
|
||||
function getWebhook (data) {
|
||||
return APIRequest.POST('/api/repository/getWebhook', data)
|
||||
}
|
||||
|
||||
function webhooks (data) {
|
||||
return APIRequest.POST('/api/repository/webhooks', data)
|
||||
}
|
||||
|
||||
function editWebhook (data) {
|
||||
return APIRequest.POST('/api/repository/editWebhook', data)
|
||||
}
|
||||
|
||||
function deleteWebhook (data) {
|
||||
return APIRequest.POST('/api/repository/deleteWebhook', data)
|
||||
}
|
||||
|
||||
function getRepositoryWebhookLogs (data) {
|
||||
return APIRequest.POST('/api/repository/getRepositoryWebhookLogs', data)
|
||||
}
|
||||
|
||||
function getRepositoryWebhookLogData (data) {
|
||||
return APIRequest.POST('/api/repository/getRepositoryWebhookLogData', data)
|
||||
}
|
||||
|
||||
export default {
|
||||
list,
|
||||
create,
|
||||
@@ -230,5 +254,11 @@ export default {
|
||||
checkMergeType,
|
||||
mergeBranch,
|
||||
mergeRequestVersionList,
|
||||
relatedMergeRequests
|
||||
relatedMergeRequests,
|
||||
getWebhook,
|
||||
webhooks,
|
||||
editWebhook,
|
||||
deleteWebhook,
|
||||
getRepositoryWebhookLogs,
|
||||
getRepositoryWebhookLogData
|
||||
}
|
||||
|
||||
@@ -42,7 +42,11 @@ const data = {
|
||||
merge_S_MergeRquest: 'Merged A Merge Request In Repository {s}',
|
||||
assign_S_Reviewer: 'Assign Reviewer In Repository {s}',
|
||||
delete_S_Reviewer: 'Delete Reviewer In Repository {s}',
|
||||
review_S_Reviewer: 'Approve Changes In Repository {s}'
|
||||
review_S_Reviewer: 'Approve Changes In Repository {s}',
|
||||
|
||||
create_S_Webhook: 'Create Webhook In Repository {s}',
|
||||
update_S_Webhook: 'Update Webhook In Repository {s}',
|
||||
delete_S_Webhook: 'Delete Webhook In Repository {s}'
|
||||
}
|
||||
|
||||
export default { ...data, __namespace__: 'message.activity' }
|
||||
|
||||
@@ -8,6 +8,7 @@ const data = {
|
||||
...Phrase,
|
||||
...Term,
|
||||
|
||||
update_S_: 'Modify {s}',
|
||||
retryAfter_N_seconds: 'Retry After {n} Seconds',
|
||||
userAvatar: [Phrase.user, Phrase.avatar].join(phraseSeperator),
|
||||
userName: [Phrase.user, Phrase.name].join(phraseSeperator),
|
||||
@@ -66,6 +67,18 @@ const data = {
|
||||
updateRepositoryURL: [Phrase.update, Term.repository, Phrase.url].join(phraseSeperator),
|
||||
updateGroupURL: [Phrase.update, Term.group, Phrase.url].join(phraseSeperator),
|
||||
|
||||
webhookSetting: [Term.webhook, Term.setting].join(phraseSeperator),
|
||||
createWebhook: [Phrase.create, Term.webhook].join(phraseSeperator),
|
||||
updateWebhook: [Phrase.update, Term.webhook].join(phraseSeperator),
|
||||
deleteWebhook: [Phrase.delete, Term.webhook].join(phraseSeperator),
|
||||
contentType: 'Content Type',
|
||||
webhookSecret: 'Secret Key',
|
||||
webhookTrigger: 'Trigger Event',
|
||||
pushTrigger: 'Just the push event',
|
||||
customeTrigger: 'Customized',
|
||||
webhookList: 'Webhook List',
|
||||
webhookLog: [Phrase.webhook, Phrase.log].join(phraseSeperator),
|
||||
|
||||
createOrigin: [Phrase.create, Phrase.origin].join(phraseSeperator),
|
||||
choseCreateOrigin: [Phrase.chose, Phrase.create, Phrase.origin].join(phraseSeperator),
|
||||
tagDescription: [Term.tag, Phrase.description].join(phraseSeperator),
|
||||
@@ -172,6 +185,13 @@ const data = {
|
||||
contribute: 'Contribute to CodeFever Community',
|
||||
about: 'About CodeFever Community',
|
||||
|
||||
pushEvent: 'Push Event',
|
||||
changeMemberRole: [Phrase.modification, Term.member, Term.role].join(phraseSeperator),
|
||||
createProtectedBranchRule: 'Create protected branch rule',
|
||||
changeProtectedBranchRule: 'Update protected branch rule',
|
||||
removeProtectedBranchRule: 'remove protected branch rule',
|
||||
reviewReviewer: 'Review Code',
|
||||
|
||||
_N_repository: '{n} {n, plural, =0 {' + Term.repository + '}\n=1 {' + Term.repository + '}\nother {' + Term.repository_pl + '}}',
|
||||
_N_commit: '{n} {n, plural, =0 {' + Term.commit + '}\n=1 {' + Term.commit + '}\nother {' + Term.commit_pl + '}}',
|
||||
_N_branch: '{n} {n, plural, =0 {' + Term.branch + '}\n=1 {' + Term.branch + '}\nother {' + Term.branch_pl + '}}',
|
||||
|
||||
@@ -150,7 +150,12 @@ const data = {
|
||||
setAdministrator: 'Set as administrator',
|
||||
cancelAdministrator: 'Cancel an administrator',
|
||||
memberRemoveConfirm: 'Member delete confirmation',
|
||||
successAddUser: 'User added successfully'
|
||||
successAddUser: 'User added successfully',
|
||||
|
||||
webhookEventsNeed: 'Please select events',
|
||||
webhookListEmpty: 'Webhook List Empty',
|
||||
useTime_n: 'Completed in {n} seconds',
|
||||
show_n_record: 'Show latest {n} records'
|
||||
}
|
||||
|
||||
export default { ...data, __namespace__: 'message' }
|
||||
|
||||
@@ -25,6 +25,11 @@ const data = {
|
||||
all: 'All',
|
||||
detail: 'Detail',
|
||||
language: 'Language',
|
||||
webhook: 'Webhook',
|
||||
log: 'Log',
|
||||
httpHeaders: 'Headers',
|
||||
httpBody: 'Body',
|
||||
httpPayload: 'Payload',
|
||||
|
||||
browser: 'View',
|
||||
expand: 'Expand',
|
||||
@@ -55,6 +60,7 @@ const data = {
|
||||
copied: 'Copied',
|
||||
contain: 'Contain',
|
||||
request: 'Request',
|
||||
response: 'Response',
|
||||
bind: 'Bind',
|
||||
unbind: 'Unbind',
|
||||
replace: 'Replace',
|
||||
|
||||
@@ -17,6 +17,9 @@ const data = {
|
||||
branch: 'Branch',
|
||||
branch_pl: 'Branches',
|
||||
|
||||
webhook: 'Webhook',
|
||||
webhook_pl: 'Webhooks',
|
||||
|
||||
tag: 'Tag',
|
||||
tag_pl: 'Tags',
|
||||
|
||||
|
||||
@@ -42,7 +42,11 @@ const data = {
|
||||
merge_S_MergeRquest: '在仓库 {s} 合并请求',
|
||||
assign_S_Reviewer: '在仓库 {s} 指定评审员',
|
||||
delete_S_Reviewer: '在仓库 {s} 删除评审员',
|
||||
review_S_Reviewer: '在仓库 {s} 评审了代码'
|
||||
review_S_Reviewer: '在仓库 {s} 评审了代码',
|
||||
|
||||
create_S_Webhook: '在仓库 {s} 创建了webhook',
|
||||
update_S_Webhook: '在仓库 {s} 更新了webhook',
|
||||
delete_S_Webhook: '在仓库 {s} 删除了webhook'
|
||||
}
|
||||
|
||||
export default { ...data, __namespace__: 'message.activity' }
|
||||
|
||||
@@ -8,6 +8,7 @@ const data = {
|
||||
...Phrase,
|
||||
...Term,
|
||||
|
||||
update_S_: '修改{s}',
|
||||
retryAfter_N_seconds: '{n} 秒后重试',
|
||||
userAvatar: [Phrase.user, Phrase.avatar].join(phraseSeperator),
|
||||
userName: [Phrase.user, Phrase.name].join(phraseSeperator),
|
||||
@@ -66,6 +67,18 @@ const data = {
|
||||
updateRepositoryURL: [Phrase.update, Term.repository, Phrase.url].join(phraseSeperator),
|
||||
updateGroupURL: [Phrase.update, Term.group, Phrase.url].join(phraseSeperator),
|
||||
|
||||
webhookSetting: [Term.webhook, Term.setting].join(phraseSeperator),
|
||||
createWebhook: [Phrase.create, Term.webhook].join(phraseSeperator),
|
||||
updateWebhook: [Phrase.update, Term.webhook].join(phraseSeperator),
|
||||
deleteWebhook: [Phrase.delete, Term.webhook].join(phraseSeperator),
|
||||
contentType: '数据格式',
|
||||
webhookSecret: '校验秘钥',
|
||||
webhookTrigger: '触发事件',
|
||||
pushTrigger: '仅推送事件',
|
||||
customeTrigger: '自定义',
|
||||
webhookList: 'Webhook列表',
|
||||
webhookLog: [Phrase.webhook, Phrase.log].join(phraseSeperator),
|
||||
|
||||
createOrigin: [Phrase.create, Phrase.origin].join(phraseSeperator),
|
||||
choseCreateOrigin: [Phrase.chose, Phrase.create, Phrase.origin].join(phraseSeperator),
|
||||
tagDescription: [Term.tag, Phrase.description].join(phraseSeperator),
|
||||
@@ -172,6 +185,13 @@ const data = {
|
||||
contribute: '为 CodeFever Community 贡献代码',
|
||||
about: '关于 CodeFever Community',
|
||||
|
||||
pushEvent: '推送事件',
|
||||
changeMemberRole: [Phrase.modification, Term.member, Term.role].join(phraseSeperator),
|
||||
createProtectedBranchRule: '创建受保护分支规则',
|
||||
changeProtectedBranchRule: '修改受保护分支规则',
|
||||
removeProtectedBranchRule: '删除受保护分支规则',
|
||||
reviewReviewer: '评审代码',
|
||||
|
||||
_N_repository: '{n} {n, plural, =0 {' + Term.repository + '}\n=1 {' + Term.repository + '}\nother {' + Term.repository_pl + '}}',
|
||||
_N_commit: '{n} {n, plural, =0 {' + Term.commit + '}\n=1 {' + Term.commit + '}\nother {' + Term.commit_pl + '}}',
|
||||
_N_branch: '{n} {n, plural, =0 {' + Term.branch + '}\n=1 {' + Term.branch + '}\nother {' + Term.branch_pl + '}}',
|
||||
|
||||
@@ -150,7 +150,12 @@ const data = {
|
||||
setAdministrator: '设置为管理员',
|
||||
cancelAdministrator: '取消管理员',
|
||||
memberRemoveConfirm: '成员删除确认',
|
||||
successAddUser: '添加用户成功'
|
||||
successAddUser: '添加用户成功',
|
||||
|
||||
webhookEventsNeed: '请选择推送事件',
|
||||
webhookListEmpty: '还没有创建Webhook',
|
||||
useTime_n: '用时 {n} s',
|
||||
show_n_record: '显示最新{n}条记录'
|
||||
}
|
||||
|
||||
export default { ...data, __namespace__: 'message' }
|
||||
|
||||
@@ -25,6 +25,11 @@ const data = {
|
||||
all: '所有',
|
||||
detail: '详情',
|
||||
language: '语言',
|
||||
webhook: 'Webhook',
|
||||
log: '日志',
|
||||
httpHeaders: '头部',
|
||||
httpBody: '响应数据',
|
||||
httpPayload: '请求数据',
|
||||
|
||||
browser: '浏览',
|
||||
expand: '展开',
|
||||
@@ -55,6 +60,7 @@ const data = {
|
||||
copied: '已复制',
|
||||
contain: '包含',
|
||||
request: '请求',
|
||||
response: '响应',
|
||||
bind: '绑定',
|
||||
unbind: '解绑',
|
||||
replace: '替换',
|
||||
|
||||
@@ -17,6 +17,9 @@ const data = {
|
||||
branch: '分支',
|
||||
branch_pl: '分支',
|
||||
|
||||
webhook: 'Webhook',
|
||||
webhook_pl: 'Webhooks',
|
||||
|
||||
tag: '标签',
|
||||
tag_pl: '标签',
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import NewRepository from 'APPSRC/components/view/NewRepository'
|
||||
import NewRepositoryFork from 'APPSRC/components/view/NewRepositoryFork'
|
||||
import RepositorySettingGeneral from 'APPSRC/components/view/RepositorySettingGeneral'
|
||||
import RepositorySettingBranch from 'APPSRC/components/view/RepositorySettingBranch'
|
||||
import RepositorySettingWebhook from 'APPSRC/components/view/RepositorySettingWebhook'
|
||||
import RepositorySettingMembers from 'APPSRC/components/view/RepositorySettingMembers'
|
||||
import RepositorySettingAdvanced from 'APPSRC/components/view/RepositorySettingAdvanced'
|
||||
|
||||
@@ -127,6 +128,7 @@ class MainRoutes extends React.Component {
|
||||
<Route exact path='/:groupName([A-Za-z0-9_]{5,})/:repositoryName([A-Za-z0-9_]+)/settings' component={RepositorySettingGeneral} />
|
||||
<Route exact path='/:groupName([A-Za-z0-9_]{5,})/:repositoryName([A-Za-z0-9_]+)/settings/general' component={RepositorySettingGeneral} />
|
||||
<Route exact path='/:groupName([A-Za-z0-9_]{5,})/:repositoryName([A-Za-z0-9_]+)/settings/branch' component={RepositorySettingBranch} />
|
||||
<Route exact path='/:groupName([A-Za-z0-9_]{5,})/:repositoryName([A-Za-z0-9_]+)/settings/webhook' component={RepositorySettingWebhook} />
|
||||
<Route exact path='/:groupName([A-Za-z0-9_]{5,})/:repositoryName([A-Za-z0-9_]+)/settings/advanced' component={RepositorySettingAdvanced} />
|
||||
|
||||
<Route component={FileTree} />
|
||||
|
||||
Reference in New Issue
Block a user