From 562056fbbe99b0d4f67be9b49581aefbc01ae30e Mon Sep 17 00:00:00 2001 From: yangchen Date: Tue, 15 Feb 2022 17:53:53 +0800 Subject: [PATCH] feat(Webhook): Support webhook (issues #24) --- application/config/access_controller.php | 4 + application/config/event_dispatchers.php | 3 +- application/config/schedule.php | 1 + application/controllers/api/repository.php | 152 +++++ .../backend/repository_webhook.php | 137 ++++ .../event_handlers/ActivityHandler.php | 39 ++ application/event_handlers/WebhookHandler.php | 62 ++ .../service/Constant/ActivityType.php | 5 + .../libraries/service/Constant/EventType.php | 10 + application/models/repository_model.php | 228 +++++++ .../components/unit/GroupRepositoryMenu.js | 2 +- .../view/RepositorySettingWebhook.js | 604 ++++++++++++++++++ .../src/components/view/unit/WebhookLog.js | 204 ++++++ www/view/src/config/Activity.js | 33 + www/view/src/config/DrawerConfig.js | 8 + www/view/src/config/WebhookEventConfig.js | 129 ++++ www/view/src/data_providers/RepositoryData.js | 32 +- www/view/src/lang/en-us/ActivityMessage.js | 6 +- www/view/src/lang/en-us/Label.js | 20 + www/view/src/lang/en-us/Message.js | 7 +- www/view/src/lang/en-us/Phrase.js | 6 + www/view/src/lang/en-us/Term.js | 3 + www/view/src/lang/zh-cn/ActivityMessage.js | 6 +- www/view/src/lang/zh-cn/Label.js | 20 + www/view/src/lang/zh-cn/Message.js | 7 +- www/view/src/lang/zh-cn/Phrase.js | 6 + www/view/src/lang/zh-cn/Term.js | 3 + www/view/src/routes/MainRoutes.js | 2 + 28 files changed, 1732 insertions(+), 7 deletions(-) create mode 100644 application/controllers/backend/repository_webhook.php create mode 100755 application/event_handlers/WebhookHandler.php create mode 100755 www/view/src/components/view/RepositorySettingWebhook.js create mode 100755 www/view/src/components/view/unit/WebhookLog.js create mode 100755 www/view/src/config/WebhookEventConfig.js diff --git a/application/config/access_controller.php b/application/config/access_controller.php index 6ed8537..62f44a7 100755 --- a/application/config/access_controller.php +++ b/application/config/access_controller.php @@ -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 ]; diff --git a/application/config/event_dispatchers.php b/application/config/event_dispatchers.php index 1447eb5..e380d93 100755 --- a/application/config/event_dispatchers.php +++ b/application/config/event_dispatchers.php @@ -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'], ] ]; } diff --git a/application/config/schedule.php b/application/config/schedule.php index 45bca8e..2501f11 100755 --- a/application/config/schedule.php +++ b/application/config/schedule.php @@ -2,4 +2,5 @@ $config['crontab'] = [ // ['crontab', 'backend|customize', 'command'] + ['* * * * *', 'backend', 'repository_webhook run'], ]; diff --git a/application/controllers/api/repository.php b/application/controllers/api/repository.php index 30935d0..ed06ec2 100755 --- a/application/controllers/api/repository.php +++ b/application/controllers/api/repository.php @@ -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); + } } diff --git a/application/controllers/backend/repository_webhook.php b/application/controllers/backend/repository_webhook.php new file mode 100644 index 0000000..397b603 --- /dev/null +++ b/application/controllers/backend/repository_webhook.php @@ -0,0 +1,137 @@ + '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; + } +} \ No newline at end of file diff --git a/application/event_handlers/ActivityHandler.php b/application/event_handlers/ActivityHandler.php index 4b934c3..710a843 100755 --- a/application/event_handlers/ActivityHandler.php +++ b/application/event_handlers/ActivityHandler.php @@ -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; + } } diff --git a/application/event_handlers/WebhookHandler.php b/application/event_handlers/WebhookHandler.php new file mode 100755 index 0000000..de05be5 --- /dev/null +++ b/application/event_handlers/WebhookHandler.php @@ -0,0 +1,62 @@ +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() + ); + } +} diff --git a/application/libraries/service/Constant/ActivityType.php b/application/libraries/service/Constant/ActivityType.php index c8c1b57..7258b2c 100755 --- a/application/libraries/service/Constant/ActivityType.php +++ b/application/libraries/service/Constant/ActivityType.php @@ -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; } diff --git a/application/libraries/service/Constant/EventType.php b/application/libraries/service/Constant/EventType.php index 79695b0..1ecee4a 100755 --- a/application/libraries/service/Constant/EventType.php +++ b/application/libraries/service/Constant/EventType.php @@ -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 = ''; diff --git a/application/models/repository_model.php b/application/models/repository_model.php index 6320e79..b359fc5 100755 --- a/application/models/repository_model.php +++ b/application/models/repository_model.php @@ -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; + } } diff --git a/www/view/src/components/unit/GroupRepositoryMenu.js b/www/view/src/components/unit/GroupRepositoryMenu.js index 0c351e4..bf4a8a1 100755 --- a/www/view/src/components/unit/GroupRepositoryMenu.js +++ b/www/view/src/components/unit/GroupRepositoryMenu.js @@ -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) => { diff --git a/www/view/src/components/view/RepositorySettingWebhook.js b/www/view/src/components/view/RepositorySettingWebhook.js new file mode 100755 index 0000000..8f7a4e9 --- /dev/null +++ b/www/view/src/components/view/RepositorySettingWebhook.js @@ -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([ + +
+
, + {item.user}, + + copyToClipboard(item.url, () => this.props.dispatchEvent(EventGenerator.NewNotification(intl.formatMessage({ id: 'label.copied' }), 0)))} + > + {item.url.substr(0, 40)} + + , + + copyToClipboard(item.secret, () => this.props.dispatchEvent(EventGenerator.NewNotification(intl.formatMessage({ id: 'label.copied' }), 0)))} + > + {item.secret.substr(0, 20)} + + , + + {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 '' + })} + , + , + + this.updateWebhook(item)} /> + this.deleteWebhook(item)} /> + this.getWebhookLogs(item.id)} /> + + ]) + 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 ( + + + {intl.formatMessage({ id: 'label.webhookSetting' })} + {!isShowWebhookForm && } + + + + {isShowWebhookForm && + + {intl.formatMessage({ id: edit ? 'label.updateWebhook' : 'label.createWebhook' })} + + + + + + + {intl.formatMessage({ id: 'label.url' })} * + + + this.setState({ url: e.target.value })} + /> + + + {intl.formatMessage({ id: 'label.contentType' })} + + + application/json + + + + {intl.formatMessage({ id: 'label.webhookSecret' })} +   + + + + + this.setState({ secret: e.target.value })} + /> + + + {intl.formatMessage({ id: 'label.webhookTrigger' })} + + + this.setState({ trigger: e.target.value })}> + } label={intl.formatMessage({ id: 'label.pushTrigger' })} /> + } label={ + + {intl.formatMessage({ id: 'label.customeTrigger' })} +   + + } /> + + + {trigger === '2' && + {events.map((item, index) => { + return 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] }) } + ) + } + /> + })} + } + + + {intl.formatMessage({ id: 'label.status' })} + + + { this.setState({ active: e.target.value }) }} row> + } label={intl.formatMessage({ id: 'label.enable' })} /> + } label={intl.formatMessage({ id: 'label.disable' })} /> + + + + + + + + + + + + } + + {!isShowWebhookForm && + + + {intl.formatMessage({ id: 'label.webhookList' })} + + {webhooks + ? webhooks.length > 0 + ? + + + : + {intl.formatMessage({ id: 'message.webhookListEmpty' })} + + : + + + } + + { + webhookLogs && + + + {intl.formatMessage({ id: 'label.webhookLog' })}   + ({intl.formatMessage({ id: 'message.show_n_record' }, { n: 30 })}) + + + + + } + } + ) + } +} + +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) + ) +) diff --git a/www/view/src/components/view/unit/WebhookLog.js b/www/view/src/components/view/unit/WebhookLog.js new file mode 100755 index 0000000..4868d1b --- /dev/null +++ b/www/view/src/components/view/unit/WebhookLog.js @@ -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 + + { + list.map(item => + + + {item.id} + copyToClipboard(item.id)} icon={plCopy} /> + + + {item.created} + { + webhookId === item.id + ? this.setState({ webhookId: '' })} icon={plClose} className={classes.more} /> + : this.getData(item.id)} icon={psMore} className={classes.more} /> + } + + { + webhookId && webhookId === item.id && logData && + + + {intl.formatMessage({ id: 'label.response' })} ]} + currentTab={webhookTab} + onChange={(e, newValue) => this.setState({ webhookTab: newValue })} + > + +   + {intl.formatMessage({ id: 'message.useTime_n' }, { n: this.getTime(item.start, item.end) })} + + + + + {intl.formatMessage({ id: 'label.httpHeaders' })} + + + + { + webhookTab === 0 + ? Object.keys(logData.request.headers).map(key => + {key}:  + {logData.request.headers[key]} + ) + : Object.keys(logData.response.headers).map(key => + {key}:  + {logData.response.headers[key]} + ) + } + + + + + {webhookTab === 0 ? intl.formatMessage({ id: 'label.httpPayload' }) : intl.formatMessage({ id: 'label.httpBody' })} +   + {webhookTab === 0 && } + + + + +
+                        
+                          {webhookTab === 0 ? JSON.stringify(JSON.parse(logData.request.body), null, 4) : logData.response.body}
+                        
+                      
+
+
+
+
+ } +
) + } + + + } +} + +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) + ) +) diff --git a/www/view/src/config/Activity.js b/www/view/src/config/Activity.js index 036e8d6..eaabf33 100755 --- a/www/view/src/config/Activity.js +++ b/www/view/src/config/Activity.js @@ -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: + {config.formatter({ id: 'label.createWebhook' })} + + } + } else if (code === 0x0902) { + return { + user, + action: config.formatter( + { id: 'message.activity.update_S_Webhook' }, + { s: config.relatedGroup.displayName + '/' + config.relatedRepository.displayName } + ), + detail: + {config.formatter({ id: 'label.updateWebhook' })} + + } + } else if (code === 0x0903) { + return { + user, + action: config.formatter( + { id: 'message.activity.delete_S_Webhook' }, + { s: config.relatedGroup.displayName + '/' + config.relatedRepository.displayName } + ), + detail: + {config.formatter({ id: 'label.deleteWebhook' })} + + } } return { diff --git a/www/view/src/config/DrawerConfig.js b/www/view/src/config/DrawerConfig.js index 526ccfe..782c6fb 100755 --- a/www/view/src/config/DrawerConfig.js +++ b/www/view/src/config/DrawerConfig.js @@ -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', diff --git a/www/view/src/config/WebhookEventConfig.js b/www/view/src/config/WebhookEventConfig.js new file mode 100755 index 0000000..4256573 --- /dev/null +++ b/www/view/src/config/WebhookEventConfig.js @@ -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 diff --git a/www/view/src/data_providers/RepositoryData.js b/www/view/src/data_providers/RepositoryData.js index 9f90990..2443b2b 100755 --- a/www/view/src/data_providers/RepositoryData.js +++ b/www/view/src/data_providers/RepositoryData.js @@ -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 } diff --git a/www/view/src/lang/en-us/ActivityMessage.js b/www/view/src/lang/en-us/ActivityMessage.js index 93b05d9..4591471 100755 --- a/www/view/src/lang/en-us/ActivityMessage.js +++ b/www/view/src/lang/en-us/ActivityMessage.js @@ -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' } diff --git a/www/view/src/lang/en-us/Label.js b/www/view/src/lang/en-us/Label.js index cc4042a..9532904 100755 --- a/www/view/src/lang/en-us/Label.js +++ b/www/view/src/lang/en-us/Label.js @@ -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 + '}}', diff --git a/www/view/src/lang/en-us/Message.js b/www/view/src/lang/en-us/Message.js index b651f72..f9e2aa4 100755 --- a/www/view/src/lang/en-us/Message.js +++ b/www/view/src/lang/en-us/Message.js @@ -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' } diff --git a/www/view/src/lang/en-us/Phrase.js b/www/view/src/lang/en-us/Phrase.js index bc9367c..e3a17ae 100755 --- a/www/view/src/lang/en-us/Phrase.js +++ b/www/view/src/lang/en-us/Phrase.js @@ -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', diff --git a/www/view/src/lang/en-us/Term.js b/www/view/src/lang/en-us/Term.js index 9aabda0..30b26d9 100755 --- a/www/view/src/lang/en-us/Term.js +++ b/www/view/src/lang/en-us/Term.js @@ -17,6 +17,9 @@ const data = { branch: 'Branch', branch_pl: 'Branches', + webhook: 'Webhook', + webhook_pl: 'Webhooks', + tag: 'Tag', tag_pl: 'Tags', diff --git a/www/view/src/lang/zh-cn/ActivityMessage.js b/www/view/src/lang/zh-cn/ActivityMessage.js index d937160..666d768 100755 --- a/www/view/src/lang/zh-cn/ActivityMessage.js +++ b/www/view/src/lang/zh-cn/ActivityMessage.js @@ -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' } diff --git a/www/view/src/lang/zh-cn/Label.js b/www/view/src/lang/zh-cn/Label.js index e9b9e3d..cfcfef5 100755 --- a/www/view/src/lang/zh-cn/Label.js +++ b/www/view/src/lang/zh-cn/Label.js @@ -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 + '}}', diff --git a/www/view/src/lang/zh-cn/Message.js b/www/view/src/lang/zh-cn/Message.js index 30c3421..fd5da89 100755 --- a/www/view/src/lang/zh-cn/Message.js +++ b/www/view/src/lang/zh-cn/Message.js @@ -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' } diff --git a/www/view/src/lang/zh-cn/Phrase.js b/www/view/src/lang/zh-cn/Phrase.js index be44874..2eeec5d 100755 --- a/www/view/src/lang/zh-cn/Phrase.js +++ b/www/view/src/lang/zh-cn/Phrase.js @@ -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: '替换', diff --git a/www/view/src/lang/zh-cn/Term.js b/www/view/src/lang/zh-cn/Term.js index 0be37ec..4ff6fd8 100755 --- a/www/view/src/lang/zh-cn/Term.js +++ b/www/view/src/lang/zh-cn/Term.js @@ -17,6 +17,9 @@ const data = { branch: '分支', branch_pl: '分支', + webhook: 'Webhook', + webhook_pl: 'Webhooks', + tag: '标签', tag_pl: '标签', diff --git a/www/view/src/routes/MainRoutes.js b/www/view/src/routes/MainRoutes.js index 7153505..8c300ab 100755 --- a/www/view/src/routes/MainRoutes.js +++ b/www/view/src/routes/MainRoutes.js @@ -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 { +