feat(Webhook): Support webhook (issues #24)

This commit is contained in:
yangchen
2022-02-15 17:53:53 +08:00
parent 9542544816
commit 562056fbbe
28 changed files with 1732 additions and 7 deletions

View File

@@ -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
];

View File

@@ -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'],
]
];
}

View File

@@ -2,4 +2,5 @@
$config['crontab'] = [
// ['crontab', 'backend|customize', 'command']
['* * * * *', 'backend', 'repository_webhook run'],
];

View File

@@ -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);
}
}

View 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;
}
}

View File

@@ -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;
}
}

View 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()
);
}
}

View File

@@ -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;
}

View File

@@ -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 = '';

View File

@@ -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;
}
}

View File

@@ -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) => {

View 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' })}
&nbsp;
<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>
&nbsp;
<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' })}&nbsp;&nbsp;
<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)
)
)

View 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' })}&emsp;<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} />&nbsp;
{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>&emsp;
<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>&emsp;
<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' })}
&nbsp;
{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)
)
)

View File

@@ -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 {

View File

@@ -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',

View 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

View File

@@ -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
}

View File

@@ -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' }

View File

@@ -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 + '}}',

View File

@@ -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' }

View File

@@ -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',

View File

@@ -17,6 +17,9 @@ const data = {
branch: 'Branch',
branch_pl: 'Branches',
webhook: 'Webhook',
webhook_pl: 'Webhooks',
tag: 'Tag',
tag_pl: 'Tags',

View File

@@ -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' }

View File

@@ -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 + '}}',

View File

@@ -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' }

View File

@@ -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: '替换',

View File

@@ -17,6 +17,9 @@ const data = {
branch: '分支',
branch_pl: '分支',
webhook: 'Webhook',
webhook_pl: 'Webhooks',
tag: '标签',
tag_pl: '标签',

View File

@@ -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} />