@@ -0,0 +1,94 @@ | |||
package controllers | |||
import ( | |||
"encoding/json" | |||
"github.com/casbin/casibase/object" | |||
) | |||
func (c *ApiController) GetGlobalProviders() { | |||
providers, err := object.GetGlobalProviders() | |||
if err != nil { | |||
c.ResponseError(err.Error()) | |||
return | |||
} | |||
c.ResponseOk(providers) | |||
} | |||
func (c *ApiController) GetProviders() { | |||
owner := c.Input().Get("owner") | |||
providers, err := object.GetProviders(owner) | |||
if err != nil { | |||
c.ResponseError(err.Error()) | |||
return | |||
} | |||
c.ResponseOk(providers) | |||
} | |||
func (c *ApiController) GetProvider() { | |||
id := c.Input().Get("id") | |||
provider, err := object.GetProvider(id) | |||
if err != nil { | |||
c.ResponseError(err.Error()) | |||
return | |||
} | |||
c.ResponseOk(provider) | |||
} | |||
func (c *ApiController) UpdateProvider() { | |||
id := c.Input().Get("id") | |||
var provider object.Provider | |||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &provider) | |||
if err != nil { | |||
c.ResponseError(err.Error()) | |||
return | |||
} | |||
success, err := object.UpdateProvider(id, &provider) | |||
if err != nil { | |||
c.ResponseError(err.Error()) | |||
return | |||
} | |||
c.ResponseOk(success) | |||
} | |||
func (c *ApiController) AddProvider() { | |||
var provider object.Provider | |||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &provider) | |||
if err != nil { | |||
c.ResponseError(err.Error()) | |||
return | |||
} | |||
success, err := object.AddProvider(&provider) | |||
if err != nil { | |||
c.ResponseError(err.Error()) | |||
return | |||
} | |||
c.ResponseOk(success) | |||
} | |||
func (c *ApiController) DeleteProvider() { | |||
var provider object.Provider | |||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &provider) | |||
if err != nil { | |||
c.ResponseError(err.Error()) | |||
return | |||
} | |||
success, err := object.DeleteProvider(&provider) | |||
if err != nil { | |||
c.ResponseError(err.Error()) | |||
return | |||
} | |||
c.ResponseOk(success) | |||
} |
@@ -108,4 +108,9 @@ func (a *Adapter) createTable() { | |||
if err != nil { | |||
panic(err) | |||
} | |||
err = a.engine.Sync2(new(Provider)) | |||
if err != nil { | |||
panic(err) | |||
} | |||
} |
@@ -0,0 +1,101 @@ | |||
package object | |||
import ( | |||
"fmt" | |||
"github.com/casbin/casibase/util" | |||
"xorm.io/core" | |||
) | |||
type Provider struct { | |||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"` | |||
Name string `xorm:"varchar(100) notnull pk" json:"name"` | |||
CreatedTime string `xorm:"varchar(100)" json:"createdTime"` | |||
DisplayName string `xorm:"varchar(100)" json:"displayName"` | |||
Category string `xorm:"varchar(100)" json:"category"` | |||
Type string `xorm:"varchar(100)" json:"type"` | |||
ClientId string `xorm:"varchar(100)" json:"clientId"` | |||
ClientSecret string `xorm:"varchar(2000)" json:"clientSecret"` | |||
ProviderUrl string `xorm:"varchar(200)" json:"providerUrl"` | |||
} | |||
func GetGlobalProviders() ([]*Provider, error) { | |||
providers := []*Provider{} | |||
err := adapter.engine.Asc("owner").Desc("created_time").Find(&providers) | |||
if err != nil { | |||
return providers, err | |||
} | |||
return providers, nil | |||
} | |||
func GetProviders(owner string) ([]*Provider, error) { | |||
providers := []*Provider{} | |||
err := adapter.engine.Desc("created_time").Find(&providers, &Provider{Owner: owner}) | |||
if err != nil { | |||
return providers, err | |||
} | |||
return providers, nil | |||
} | |||
func getProvider(owner string, name string) (*Provider, error) { | |||
provider := Provider{Owner: owner, Name: name} | |||
existed, err := adapter.engine.Get(&provider) | |||
if err != nil { | |||
return &provider, err | |||
} | |||
if existed { | |||
return &provider, nil | |||
} else { | |||
return nil, nil | |||
} | |||
} | |||
func GetProvider(id string) (*Provider, error) { | |||
owner, name := util.GetOwnerAndNameFromId(id) | |||
return getProvider(owner, name) | |||
} | |||
func UpdateProvider(id string, provider *Provider) (bool, error) { | |||
owner, name := util.GetOwnerAndNameFromId(id) | |||
_, err := getProvider(owner, name) | |||
if err != nil { | |||
return false, err | |||
} | |||
if provider == nil { | |||
return false, nil | |||
} | |||
_, err = adapter.engine.ID(core.PK{owner, name}).AllCols().Update(provider) | |||
if err != nil { | |||
return false, err | |||
} | |||
//return affected != 0 | |||
return true, nil | |||
} | |||
func AddProvider(provider *Provider) (bool, error) { | |||
affected, err := adapter.engine.Insert(provider) | |||
if err != nil { | |||
return false, err | |||
} | |||
return affected != 0, nil | |||
} | |||
func DeleteProvider(provider *Provider) (bool, error) { | |||
affected, err := adapter.engine.ID(core.PK{provider.Owner, provider.Name}).Delete(&Provider{}) | |||
if err != nil { | |||
return false, err | |||
} | |||
return affected != 0, nil | |||
} | |||
func (provider *Provider) GetId() string { | |||
return fmt.Sprintf("%s/%s", provider.Owner, provider.Name) | |||
} |
@@ -53,6 +53,13 @@ func initAPI() { | |||
beego.Router("/api/add-store", &controllers.ApiController{}, "POST:AddStore") | |||
beego.Router("/api/delete-store", &controllers.ApiController{}, "POST:DeleteStore") | |||
beego.Router("/api/get-global-providers", &controllers.ApiController{}, "GET:GetGlobalProviders") | |||
beego.Router("/api/get-providers", &controllers.ApiController{}, "GET:GetProviders") | |||
beego.Router("/api/get-provider", &controllers.ApiController{}, "GET:GetProvider") | |||
beego.Router("/api/update-provider", &controllers.ApiController{}, "POST:UpdateProvider") | |||
beego.Router("/api/add-provider", &controllers.ApiController{}, "POST:AddProvider") | |||
beego.Router("/api/delete-provider", &controllers.ApiController{}, "POST:DeleteProvider") | |||
beego.Router("/api/update-file", &controllers.ApiController{}, "POST:UpdateFile") | |||
beego.Router("/api/add-file", &controllers.ApiController{}, "POST:AddFile") | |||
beego.Router("/api/delete-file", &controllers.ApiController{}, "POST:DeleteFile") | |||
@@ -19,6 +19,8 @@ import VectorsetListPage from "./VectorsetListPage"; | |||
import VectorsetEditPage from "./VectorsetEditPage"; | |||
import VideoListPage from "./VideoListPage"; | |||
import VideoEditPage from "./VideoEditPage"; | |||
import ProviderListPage from "./ProviderListPage"; | |||
import ProviderEditPage from "./ProviderEditPage"; | |||
import SigninPage from "./SigninPage"; | |||
import i18next from "i18next"; | |||
import LanguageSelect from "./LanguageSelect"; | |||
@@ -70,6 +72,8 @@ class App extends Component { | |||
this.setState({selectedMenuKey: "/vectorsets"}); | |||
} else if (uri.includes("/videos")) { | |||
this.setState({selectedMenuKey: "/videos"}); | |||
} else if (uri.includes("/providers")) { | |||
this.setState({selectedMenuKey: "/providers"}); | |||
} else { | |||
this.setState({selectedMenuKey: "null"}); | |||
} | |||
@@ -289,6 +293,13 @@ class App extends Component { | |||
</Link> | |||
</Menu.Item> | |||
); | |||
res.push( | |||
<Menu.Item key="/providers"> | |||
<Link to="/providers"> | |||
{i18next.t("general:Providers")} | |||
</Link> | |||
</Menu.Item> | |||
); | |||
if (Setting.isLocalAdminUser(this.state.account)) { | |||
res.push( | |||
@@ -363,6 +374,8 @@ class App extends Component { | |||
<Route exact path="/vectorsets/:vectorsetName" render={(props) => this.renderSigninIfNotSignedIn(<VectorsetEditPage account={this.state.account} {...props} />)} /> | |||
<Route exact path="/videos" render={(props) => this.renderSigninIfNotSignedIn(<VideoListPage account={this.state.account} {...props} />)} /> | |||
<Route exact path="/videos/:videoName" render={(props) => this.renderSigninIfNotSignedIn(<VideoEditPage account={this.state.account} {...props} />)} /> | |||
<Route exact path="/providers" render={(props) => this.renderSigninIfNotSignedIn(<ProviderListPage account={this.state.account} {...props} />)} /> | |||
<Route exact path="/providers/:providerName" render={(props) => this.renderSigninIfNotSignedIn(<ProviderEditPage account={this.state.account} {...props} />)} /> | |||
</Switch> | |||
</div> | |||
); | |||
@@ -0,0 +1,185 @@ | |||
import React from "react"; | |||
import {Button, Card, Col, Input, Row, Select} from "antd"; | |||
import * as ProviderBackend from "./backend/ProviderBackend"; | |||
import * as Setting from "./Setting"; | |||
import i18next from "i18next"; | |||
import {LinkOutlined} from "@ant-design/icons"; | |||
const {Option} = Select; | |||
class ProviderEditPage extends React.Component { | |||
constructor(props) { | |||
super(props); | |||
this.state = { | |||
classes: props, | |||
providerName: props.match.params.providerName, | |||
provider: null, | |||
}; | |||
} | |||
UNSAFE_componentWillMount() { | |||
this.getProvider(); | |||
} | |||
getProvider() { | |||
ProviderBackend.getProvider(this.props.account.name, this.state.providerName) | |||
.then((provider) => { | |||
if (provider.status === "ok") { | |||
this.setState({ | |||
provider: provider.data, | |||
}); | |||
} else { | |||
Setting.showMessage("error", `Failed to get provider: ${provider.msg}`); | |||
} | |||
}); | |||
} | |||
parseProviderField(key, value) { | |||
if ([""].includes(key)) { | |||
value = Setting.myParseInt(value); | |||
} | |||
return value; | |||
} | |||
updateProviderField(key, value) { | |||
value = this.parseProviderField(key, value); | |||
const provider = this.state.provider; | |||
provider[key] = value; | |||
this.setState({ | |||
provider: provider, | |||
}); | |||
} | |||
renderProvider() { | |||
return ( | |||
<Card size="small" title={ | |||
<div> | |||
{i18next.t("provider:Edit Provider")} | |||
<Button type="primary" onClick={this.submitProviderEdit.bind(this)}>{i18next.t("general:Save")}</Button> | |||
</div> | |||
} style={{marginLeft: "5px"}} type="inner"> | |||
<Row style={{marginTop: "10px"}} > | |||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> | |||
{i18next.t("general:Name")}: | |||
</Col> | |||
<Col span={22} > | |||
<Input value={this.state.provider.name} onChange={e => { | |||
this.updateProviderField("name", e.target.value); | |||
}} /> | |||
</Col> | |||
</Row> | |||
<Row style={{marginTop: "20px"}} > | |||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> | |||
{i18next.t("general:Display name")}: | |||
</Col> | |||
<Col span={22} > | |||
<Input value={this.state.provider.displayName} onChange={e => { | |||
this.updateProviderField("displayName", e.target.value); | |||
}} /> | |||
</Col> | |||
</Row> | |||
<Row style={{marginTop: "20px"}} > | |||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> | |||
{i18next.t("provider:Category")}: | |||
</Col> | |||
<Col span={22} > | |||
<Select virtual={false} style={{width: "100%"}} value={this.state.provider.category} onChange={(value => {this.updateProviderField("category", value);})}> | |||
{ | |||
[ | |||
{id: "AI", name: "AI"}, | |||
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>) | |||
} | |||
</Select> | |||
</Col> | |||
</Row> | |||
<Row style={{marginTop: "20px"}} > | |||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> | |||
{i18next.t("provider:Type")}: | |||
</Col> | |||
<Col span={22} > | |||
<Select virtual={false} style={{width: "100%"}} value={this.state.provider.type} onChange={(value => {this.updateProviderField("type", value);})}> | |||
{ | |||
[ | |||
{id: "OpenAI API - GPT 3.5", name: "OpenAI API - GPT 3.5"}, | |||
{id: "OpenAI API - GPT 4", name: "OpenAI API - GPT 4"}, | |||
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>) | |||
} | |||
</Select> | |||
</Col> | |||
</Row> | |||
<Row style={{marginTop: "20px"}} > | |||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> | |||
{i18next.t("provider:Secret key")}: | |||
</Col> | |||
<Col span={22} > | |||
<Input value={this.state.provider.clientSecret} onChange={e => { | |||
this.updateProviderField("clientSecret", e.target.value); | |||
}} /> | |||
</Col> | |||
</Row> | |||
<Row style={{marginTop: "20px"}} > | |||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> | |||
{i18next.t("general:Provider URL")}: | |||
</Col> | |||
<Col span={22} > | |||
<Input prefix={<LinkOutlined />} value={this.state.provider.providerUrl} onChange={e => { | |||
this.updateProviderField("providerUrl", e.target.value); | |||
}} /> | |||
</Col> | |||
</Row> | |||
</Card> | |||
); | |||
} | |||
submitProviderEdit() { | |||
const provider = Setting.deepCopy(this.state.provider); | |||
ProviderBackend.updateProvider(this.state.provider.owner, this.state.providerName, provider) | |||
.then((res) => { | |||
if (res.status === "ok") { | |||
if (res.data) { | |||
Setting.showMessage("success", "Successfully saved"); | |||
this.setState({ | |||
providerName: this.state.provider.name, | |||
}); | |||
this.props.history.push(`/providers/${this.state.provider.name}`); | |||
} else { | |||
Setting.showMessage("error", "failed to save: server side failure"); | |||
this.updateProviderField("name", this.state.providerName); | |||
} | |||
} else { | |||
Setting.showMessage("error", `failed to save: ${res.msg}`); | |||
} | |||
}) | |||
.catch(error => { | |||
Setting.showMessage("error", `failed to save: ${error}`); | |||
}); | |||
} | |||
render() { | |||
return ( | |||
<div> | |||
<Row style={{width: "100%"}}> | |||
<Col span={1}> | |||
</Col> | |||
<Col span={22}> | |||
{ | |||
this.state.provider !== null ? this.renderProvider() : null | |||
} | |||
</Col> | |||
<Col span={1}> | |||
</Col> | |||
</Row> | |||
<Row style={{margin: 10}}> | |||
<Col span={2}> | |||
</Col> | |||
<Col span={18}> | |||
<Button type="primary" size="large" onClick={this.submitProviderEdit.bind(this)}>{i18next.t("general:Save")}</Button> | |||
</Col> | |||
</Row> | |||
</div> | |||
); | |||
} | |||
} | |||
export default ProviderEditPage; |
@@ -0,0 +1,202 @@ | |||
import React from "react"; | |||
import {Link} from "react-router-dom"; | |||
import {Button, Col, Popconfirm, Row, Table} from "antd"; | |||
import moment from "moment"; | |||
import * as Setting from "./Setting"; | |||
import * as ProviderBackend from "./backend/ProviderBackend"; | |||
import i18next from "i18next"; | |||
class ProviderListPage extends React.Component { | |||
constructor(props) { | |||
super(props); | |||
this.state = { | |||
classes: props, | |||
providers: null, | |||
}; | |||
} | |||
UNSAFE_componentWillMount() { | |||
this.getProviders(); | |||
} | |||
getProviders() { | |||
ProviderBackend.getProviders(this.props.account.name) | |||
.then((res) => { | |||
if (res.status === "ok") { | |||
this.setState({ | |||
providers: res.data, | |||
}); | |||
} else { | |||
Setting.showMessage("error", `Failed to get providers: ${res.msg}`); | |||
} | |||
}); | |||
} | |||
newProvider() { | |||
const randomName = Setting.getRandomName(); | |||
return { | |||
owner: this.props.account.name, | |||
name: `provider_${randomName}`, | |||
createdTime: moment().format(), | |||
displayName: `New Provider - ${randomName}`, | |||
category: "AI", | |||
type: "OpenAI API - GPT 3.5", | |||
clientId: "", | |||
clientSecret: "", | |||
providerUrl: "https://platform.openai.com/account/api-keys", | |||
}; | |||
} | |||
addProvider() { | |||
const newProvider = this.newProvider(); | |||
ProviderBackend.addProvider(newProvider) | |||
.then((res) => { | |||
if (res.status === "ok") { | |||
Setting.showMessage("success", "Provider added successfully"); | |||
this.setState({ | |||
providers: Setting.prependRow(this.state.providers, newProvider), | |||
}); | |||
} else { | |||
Setting.showMessage("error", `Failed to add provider: ${res.msg}`); | |||
} | |||
}) | |||
.catch(error => { | |||
Setting.showMessage("error", `Provider failed to add: ${error}`); | |||
}); | |||
} | |||
deleteProvider(i) { | |||
ProviderBackend.deleteProvider(this.state.providers[i]) | |||
.then((res) => { | |||
if (res.status === "ok") { | |||
Setting.showMessage("success", "Provider deleted successfully"); | |||
this.setState({ | |||
providers: Setting.deleteRow(this.state.providers, i), | |||
}); | |||
} else { | |||
Setting.showMessage("error", `Provider failed to delete: ${res.msg}`); | |||
} | |||
}) | |||
.catch(error => { | |||
Setting.showMessage("error", `Provider failed to delete: ${error}`); | |||
}); | |||
} | |||
renderTable(providers) { | |||
const columns = [ | |||
{ | |||
title: i18next.t("general:Name"), | |||
dataIndex: "name", | |||
key: "name", | |||
width: "140px", | |||
sorter: (a, b) => a.name.localeCompare(b.name), | |||
render: (text, record, index) => { | |||
return ( | |||
<Link to={`/providers/${text}`}> | |||
{text} | |||
</Link> | |||
); | |||
}, | |||
}, | |||
{ | |||
title: i18next.t("general:Display name"), | |||
dataIndex: "displayName", | |||
key: "displayName", | |||
width: "200px", | |||
sorter: (a, b) => a.displayName.localeCompare(b.displayName), | |||
}, | |||
{ | |||
title: i18next.t("provider:Category"), | |||
dataIndex: "category", | |||
key: "category", | |||
width: "200px", | |||
sorter: (a, b) => a.category.localeCompare(b.category), | |||
}, | |||
{ | |||
title: i18next.t("provider:Type"), | |||
dataIndex: "type", | |||
key: "type", | |||
width: "200px", | |||
sorter: (a, b) => a.type.localeCompare(b.type), | |||
}, | |||
{ | |||
title: i18next.t("provider:Secret key"), | |||
dataIndex: "clientSecret", | |||
key: "clientSecret", | |||
width: "200px", | |||
sorter: (a, b) => a.clientSecret.localeCompare(b.clientSecret), | |||
}, | |||
{ | |||
title: i18next.t("provider:Provider URL"), | |||
dataIndex: "providerUrl", | |||
key: "providerUrl", | |||
width: "250px", | |||
sorter: (a, b) => a.providerUrl.localeCompare(b.providerUrl), | |||
render: (text, record, index) => { | |||
return ( | |||
<a target="_blank" rel="noreferrer" href={text}> | |||
{ | |||
Setting.getShortText(text) | |||
} | |||
</a> | |||
); | |||
}, | |||
}, | |||
{ | |||
title: i18next.t("general:Action"), | |||
dataIndex: "action", | |||
key: "action", | |||
width: "180px", | |||
render: (text, record, index) => { | |||
return ( | |||
<div> | |||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/providers/${record.name}`)}>{i18next.t("general:Edit")}</Button> | |||
<Popconfirm | |||
title={`Sure to delete provider: ${record.name} ?`} | |||
onConfirm={() => this.deleteProvider(index)} | |||
okText="OK" | |||
cancelText="Cancel" | |||
> | |||
<Button style={{marginBottom: "10px"}} type="danger">{i18next.t("general:Delete")}</Button> | |||
</Popconfirm> | |||
</div> | |||
); | |||
}, | |||
}, | |||
]; | |||
return ( | |||
<div> | |||
<Table columns={columns} dataSource={providers} rowKey="name" size="middle" bordered pagination={{pageSize: 100}} | |||
title={() => ( | |||
<div> | |||
{i18next.t("general:Providers")} | |||
<Button type="primary" size="small" onClick={this.addProvider.bind(this)}>{i18next.t("general:Add")}</Button> | |||
</div> | |||
)} | |||
loading={providers === null} | |||
/> | |||
</div> | |||
); | |||
} | |||
render() { | |||
return ( | |||
<div> | |||
<Row style={{width: "100%"}}> | |||
<Col span={1}> | |||
</Col> | |||
<Col span={22}> | |||
{ | |||
this.renderTable(this.state.providers) | |||
} | |||
</Col> | |||
<Col span={1}> | |||
</Col> | |||
</Row> | |||
</div> | |||
); | |||
} | |||
} | |||
export default ProviderListPage; |
@@ -0,0 +1,56 @@ | |||
import * as Setting from "../Setting"; | |||
export function getGlobalProviders() { | |||
return fetch(`${Setting.ServerUrl}/api/get-global-providers`, { | |||
method: "GET", | |||
credentials: "include", | |||
}).then(res => res.json()); | |||
} | |||
export function getProviders(owner) { | |||
return fetch(`${Setting.ServerUrl}/api/get-providers?owner=${owner}`, { | |||
method: "GET", | |||
credentials: "include", | |||
}).then(res => res.json()); | |||
} | |||
export function getProvider(owner, name) { | |||
return fetch(`${Setting.ServerUrl}/api/get-provider?id=${owner}/${encodeURIComponent(name)}`, { | |||
method: "GET", | |||
credentials: "include", | |||
}).then(res => res.json()); | |||
} | |||
export function getProviderGraph(owner, name, clusterNumber, distanceLimit) { | |||
return fetch(`${Setting.ServerUrl}/api/get-provider-graph?id=${owner}/${encodeURIComponent(name)}&clusterNumber=${clusterNumber}&distanceLimit=${distanceLimit}`, { | |||
method: "GET", | |||
credentials: "include", | |||
}).then(res => res.json()); | |||
} | |||
export function updateProvider(owner, name, provider) { | |||
const newProvider = Setting.deepCopy(provider); | |||
return fetch(`${Setting.ServerUrl}/api/update-provider?id=${owner}/${encodeURIComponent(name)}`, { | |||
method: "POST", | |||
credentials: "include", | |||
body: JSON.stringify(newProvider), | |||
}).then(res => res.json()); | |||
} | |||
export function addProvider(provider) { | |||
const newProvider = Setting.deepCopy(provider); | |||
return fetch(`${Setting.ServerUrl}/api/add-provider`, { | |||
method: "POST", | |||
credentials: "include", | |||
body: JSON.stringify(newProvider), | |||
}).then(res => res.json()); | |||
} | |||
export function deleteProvider(provider) { | |||
const newProvider = Setting.deepCopy(provider); | |||
return fetch(`${Setting.ServerUrl}/api/delete-provider`, { | |||
method: "POST", | |||
credentials: "include", | |||
body: JSON.stringify(newProvider), | |||
}).then(res => res.json()); | |||
} |