Add webhook pages.

This commit is contained in:
Gucheng Wang
2021-11-07 15:41:24 +08:00
parent cbf973882d
commit 0e71e603ac
9 changed files with 742 additions and 1 deletions

View File

@ -34,6 +34,8 @@ import LdapSyncPage from "./LdapSyncPage";
import TokenListPage from "./TokenListPage";
import TokenEditPage from "./TokenEditPage";
import RecordListPage from "./RecordListPage";
import WebhookListPage from "./WebhookListPage";
import WebhookEditPage from "./WebhookEditPage";
import AccountPage from "./account/AccountPage";
import HomePage from "./basic/HomePage";
import CustomGithubCorner from "./CustomGithubCorner";
@ -107,6 +109,8 @@ class App extends Component {
this.setState({ selectedMenuKey: '/tokens' });
} else if (uri.includes('/records')) {
this.setState({ selectedMenuKey: '/records' });
} else if (uri.includes('/webhooks')) {
this.setState({ selectedMenuKey: '/webhooks' });
} else if (uri.includes('/signup')) {
this.setState({ selectedMenuKey: '/signup' });
} else if (uri.includes('/login')) {
@ -354,6 +358,13 @@ class App extends Component {
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/webhooks">
<Link to="/webhooks">
{i18next.t("general:Webhooks")}
</Link>
</Menu.Item>
);
res.push(
<Menu.Item key="/swagger">
<a target="_blank" rel="noreferrer" href={Setting.isLocalhost() ? `${Setting.ServerUrl}/swagger` : "/swagger"}>
@ -414,6 +425,8 @@ class App extends Component {
<Route exact path="/ldap/sync/:ldapId" render={(props) => this.renderLoginIfNotLoggedIn(<LdapSyncPage account={this.state.account} {...props} />)}/>
<Route exact path="/tokens" render={(props) => this.renderLoginIfNotLoggedIn(<TokenListPage account={this.state.account} {...props} />)}/>
<Route exact path="/tokens/:tokenName" render={(props) => this.renderLoginIfNotLoggedIn(<TokenEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/webhooks" render={(props) => this.renderLoginIfNotLoggedIn(<WebhookListPage account={this.state.account} {...props} />)}/>
<Route exact path="/webhooks/:webhookName" render={(props) => this.renderLoginIfNotLoggedIn(<WebhookEditPage account={this.state.account} {...props} />)}/>
<Route exact path="/records" render={(props) => this.renderLoginIfNotLoggedIn(<RecordListPage account={this.state.account} {...props} />)}/>
<Route exact path="/.well-known/openid-configuration" render={(props) => <OdicDiscoveryPage />}/>
<Route path="" render={() => <Result status="404" title="404 NOT FOUND" subTitle={i18next.t("general:Sorry, the page you visited does not exist.")}

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import {message, Tooltip} from "antd";
import {message, Tag, Tooltip} from "antd";
import {QuestionCircleTwoTone} from "@ant-design/icons";
import React from "react";
import {isMobile as isMobileDevice} from "react-device-detect";
@ -511,3 +511,19 @@ export function getDeduplicatedArray(array, filterArray, key) {
const res = array.filter(item => filterArray.filter(filterItem => filterItem[key] === item[key]).length === 0);
return res;
}
export function getTagColor(s) {
return "success";
}
export function getTags(tags) {
let res = [];
tags.forEach((tag, i) => {
res.push(
<Tag color={getTagColor(tag)}>
{tag}
</Tag>
);
});
return res;
}

191
web/src/WebhookEditPage.js Normal file
View File

@ -0,0 +1,191 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Button, Card, Col, Input, Row, Select} from 'antd';
import {LinkOutlined} from "@ant-design/icons";
import * as WebhookBackend from "./backend/WebhookBackend";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
const { Option } = Select;
class WebhookEditPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
webhookName: props.match.params.webhookName,
webhook: null,
organizations: [],
};
}
UNSAFE_componentWillMount() {
this.getWebhook();
this.getOrganizations();
}
getWebhook() {
WebhookBackend.getWebhook("admin", this.state.webhookName)
.then((webhook) => {
this.setState({
webhook: webhook,
});
});
}
getOrganizations() {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: (res.msg === undefined) ? res : [],
});
});
}
parseWebhookField(key, value) {
if (["port"].includes(key)) {
value = Setting.myParseInt(value);
}
return value;
}
updateWebhookField(key, value) {
value = this.parseWebhookField(key, value);
let webhook = this.state.webhook;
webhook[key] = value;
this.setState({
webhook: webhook,
});
}
renderWebhook() {
return (
<Card size="small" title={
<div>
{i18next.t("webhook:Edit Webhook")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" onClick={this.submitWebhookEdit.bind(this)}>{i18next.t("general:Save")}</Button>
</div>
} style={(Setting.isMobile())? {margin: '5px'}:{}} type="inner">
<Row style={{marginTop: '10px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.webhook.organization} onChange={(value => {this.updateWebhookField('organization', value);})}>
{
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.webhook.name} onChange={e => {
this.updateWebhookField('name', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("webhook:URL"), i18next.t("webhook:URL - Tooltip"))} :
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined/>} value={this.state.webhook.url} onChange={e => {
this.updateWebhookField('url', e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("webhook:Content type"), i18next.t("webhook:Content type - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: '100%'}} value={this.state.webhook.contentType} onChange={(value => {this.updateWebhookField('contentType', value);})}>
{
[
{id: 'application/json', name: 'application/json'},
{id: 'application/x-www-form-urlencoded', name: 'application/x-www-form-urlencoded'},
].map((contentType, index) => <Option key={index} value={contentType.id}>{contentType.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: '20px'}} >
<Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("webhook:Events"), i18next.t("webhook:Events - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} mode="tags" style={{width: '100%'}}
value={this.state.webhook.events}
onChange={value => {
this.updateWebhookField('events', value);
}} >
{
(
["signup", "login", "logout", "update-user"].map((option, index) => {
return (
<Option key={option} value={option}>{option}</Option>
)
})
)
}
</Select>
</Col>
</Row>
</Card>
)
}
submitWebhookEdit() {
let webhook = Setting.deepCopy(this.state.webhook);
WebhookBackend.updateWebhook(this.state.webhook.owner, this.state.webhookName, webhook)
.then((res) => {
if (res.msg === "") {
Setting.showMessage("success", `Successfully saved`);
this.setState({
webhookName: this.state.webhook.name,
});
this.props.history.push(`/webhooks/${this.state.webhook.name}`);
} else {
Setting.showMessage("error", res.msg);
this.updateWebhookField('name', this.state.webhookName);
}
})
.catch(error => {
Setting.showMessage("error", `Failed to connect to server: ${error}`);
});
}
render() {
return (
<div>
{
this.state.webhook !== null ? this.renderWebhook() : null
}
<div style={{marginTop: '20px', marginLeft: '40px'}}>
<Button type="primary" size="large" onClick={this.submitWebhookEdit.bind(this)}>{i18next.t("general:Save")}</Button>
</div>
</div>
);
}
}
export default WebhookEditPage;

224
web/src/WebhookListPage.js Normal file
View File

@ -0,0 +1,224 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import {Link} from "react-router-dom";
import {Button, Popconfirm, Table} from 'antd';
import moment from "moment";
import * as Setting from "./Setting";
import * as WebhookBackend from "./backend/WebhookBackend";
import i18next from "i18next";
class WebhookListPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
webhooks: null,
total: 0,
};
}
UNSAFE_componentWillMount() {
this.getWebhooks(1, 10);
}
getWebhooks(page, pageSize) {
WebhookBackend.getWebhooks("admin", page, pageSize)
.then((res) => {
if (res.status === "ok") {
this.setState({
webhooks: res.data,
total: res.data2
});
}
});
}
newWebhook() {
var randomName = Math.random().toString(36).slice(-6)
return {
owner: "admin", // this.props.account.webhookname,
name: `webhook_${randomName}`,
createdTime: moment().format(),
url: "https://example.com/callback",
contentType: "application/json",
events: [],
organization: "built-in",
}
}
addWebhook() {
const newWebhook = this.newWebhook();
WebhookBackend.addWebhook(newWebhook)
.then((res) => {
Setting.showMessage("success", `Webhook added successfully`);
this.setState({
webhooks: Setting.prependRow(this.state.webhooks, newWebhook),
total: this.state.total + 1
});
}
)
.catch(error => {
Setting.showMessage("error", `Webhook failed to add: ${error}`);
});
}
deleteWebhook(i) {
WebhookBackend.deleteWebhook(this.state.webhooks[i])
.then((res) => {
Setting.showMessage("success", `Webhook deleted successfully`);
this.setState({
webhooks: Setting.deleteRow(this.state.webhooks, i),
total: this.state.total - 1
});
}
)
.catch(error => {
Setting.showMessage("error", `Webhook failed to delete: ${error}`);
});
}
renderTable(webhooks) {
const columns = [
{
title: i18next.t("general:Organization"),
dataIndex: 'organization',
key: 'organization',
width: '80px',
sorter: (a, b) => a.organization.localeCompare(b.organization),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
)
}
},
{
title: i18next.t("general:Name"),
dataIndex: 'name',
key: 'name',
width: '150px',
fixed: 'left',
sorter: (a, b) => a.name.localeCompare(b.name),
render: (text, record, index) => {
return (
<Link to={`/webhooks/${text}`}>
{text}
</Link>
)
}
},
{
title: i18next.t("general:Created time"),
dataIndex: 'createdTime',
key: 'createdTime',
width: '180px',
sorter: (a, b) => a.createdTime.localeCompare(b.createdTime),
render: (text, record, index) => {
return Setting.getFormattedDate(text);
}
},
{
title: i18next.t("webhook:URL"),
dataIndex: 'url',
key: 'url',
width: '300px',
sorter: (a, b) => a.url.localeCompare(b.url),
render: (text, record, index) => {
return (
<a target="_blank" rel="noreferrer" href={text}>
{
Setting.getShortText(text)
}
</a>
)
}
},
{
title: i18next.t("webhook:Content type"),
dataIndex: 'contentType',
key: 'contentType',
width: '150px',
sorter: (a, b) => a.contentType.localeCompare(b.contentType),
},
{
title: i18next.t("webhook:Events"),
dataIndex: 'events',
key: 'events',
// width: '100px',
sorter: (a, b) => a.events.localeCompare(b.events),
render: (text, record, index) => {
return Setting.getTags(text);
}
},
{
title: i18next.t("general:Action"),
dataIndex: '',
key: 'op',
width: '170px',
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: '10px', marginBottom: '10px', marginRight: '10px'}} type="primary" onClick={() => this.props.history.push(`/webhooks/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<Popconfirm
title={`Sure to delete webhook: ${record.name} ?`}
onConfirm={() => this.deleteWebhook(index)}
>
<Button style={{marginBottom: '10px'}} type="danger">{i18next.t("general:Delete")}</Button>
</Popconfirm>
</div>
)
}
},
];
const paginationProps = {
total: this.state.total,
showQuickJumper: true,
showSizeChanger: true,
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.total),
onChange: (page, pageSize) => this.getWebhooks(page, pageSize),
onShowSizeChange: (current, size) => this.getWebhooks(current, size),
};
return (
<div>
<Table scroll={{x: 'max-content'}} columns={columns} dataSource={webhooks} rowKey="name" size="middle" bordered pagination={paginationProps}
title={() => (
<div>
{i18next.t("general:Webhooks")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" onClick={this.addWebhook.bind(this)}>{i18next.t("general:Add")}</Button>
</div>
)}
loading={webhooks === null}
/>
</div>
);
}
render() {
return (
<div>
{
this.renderTable(this.state.webhooks)
}
</div>
);
}
}
export default WebhookListPage;

View File

@ -0,0 +1,56 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as Setting from "../Setting";
export function getWebhooks(owner, page = "", pageSize = "") {
return fetch(`${Setting.ServerUrl}/api/get-webhooks?owner=${owner}&p=${page}&pageSize=${pageSize}`, {
method: "GET",
credentials: "include"
}).then(res => res.json());
}
export function getWebhook(owner, name) {
return fetch(`${Setting.ServerUrl}/api/get-webhook?id=${owner}/${encodeURIComponent(name)}`, {
method: "GET",
credentials: "include"
}).then(res => res.json());
}
export function updateWebhook(owner, name, webhook) {
let newWebhook = Setting.deepCopy(webhook);
return fetch(`${Setting.ServerUrl}/api/update-webhook?id=${owner}/${encodeURIComponent(name)}`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(newWebhook),
}).then(res => res.json());
}
export function addWebhook(webhook) {
let newWebhook = Setting.deepCopy(webhook);
return fetch(`${Setting.ServerUrl}/api/add-webhook`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(newWebhook),
}).then(res => res.json());
}
export function deleteWebhook(webhook) {
let newWebhook = Setting.deepCopy(webhook);
return fetch(`${Setting.ServerUrl}/api/delete-webhook`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(newWebhook),
}).then(res => res.json());
}