Browse Source

Add provider pages

HEAD
Yang Luo 2 years ago
parent
commit
ac76b1fb9b
8 changed files with 663 additions and 0 deletions
  1. +94
    -0
      controllers/provider.go
  2. +5
    -0
      object/adapter.go
  3. +101
    -0
      object/provider.go
  4. +7
    -0
      routers/router.go
  5. +13
    -0
      web/src/App.js
  6. +185
    -0
      web/src/ProviderEditPage.js
  7. +202
    -0
      web/src/ProviderListPage.js
  8. +56
    -0
      web/src/backend/ProviderBackend.js

+ 94
- 0
controllers/provider.go View File

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

+ 5
- 0
object/adapter.go View File

@@ -108,4 +108,9 @@ func (a *Adapter) createTable() {
if err != nil { if err != nil {
panic(err) panic(err)
} }

err = a.engine.Sync2(new(Provider))
if err != nil {
panic(err)
}
} }

+ 101
- 0
object/provider.go View File

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

+ 7
- 0
routers/router.go View File

@@ -53,6 +53,13 @@ func initAPI() {
beego.Router("/api/add-store", &controllers.ApiController{}, "POST:AddStore") beego.Router("/api/add-store", &controllers.ApiController{}, "POST:AddStore")
beego.Router("/api/delete-store", &controllers.ApiController{}, "POST:DeleteStore") 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/update-file", &controllers.ApiController{}, "POST:UpdateFile")
beego.Router("/api/add-file", &controllers.ApiController{}, "POST:AddFile") beego.Router("/api/add-file", &controllers.ApiController{}, "POST:AddFile")
beego.Router("/api/delete-file", &controllers.ApiController{}, "POST:DeleteFile") beego.Router("/api/delete-file", &controllers.ApiController{}, "POST:DeleteFile")


+ 13
- 0
web/src/App.js View File

@@ -19,6 +19,8 @@ import VectorsetListPage from "./VectorsetListPage";
import VectorsetEditPage from "./VectorsetEditPage"; import VectorsetEditPage from "./VectorsetEditPage";
import VideoListPage from "./VideoListPage"; import VideoListPage from "./VideoListPage";
import VideoEditPage from "./VideoEditPage"; import VideoEditPage from "./VideoEditPage";
import ProviderListPage from "./ProviderListPage";
import ProviderEditPage from "./ProviderEditPage";
import SigninPage from "./SigninPage"; import SigninPage from "./SigninPage";
import i18next from "i18next"; import i18next from "i18next";
import LanguageSelect from "./LanguageSelect"; import LanguageSelect from "./LanguageSelect";
@@ -70,6 +72,8 @@ class App extends Component {
this.setState({selectedMenuKey: "/vectorsets"}); this.setState({selectedMenuKey: "/vectorsets"});
} else if (uri.includes("/videos")) { } else if (uri.includes("/videos")) {
this.setState({selectedMenuKey: "/videos"}); this.setState({selectedMenuKey: "/videos"});
} else if (uri.includes("/providers")) {
this.setState({selectedMenuKey: "/providers"});
} else { } else {
this.setState({selectedMenuKey: "null"}); this.setState({selectedMenuKey: "null"});
} }
@@ -289,6 +293,13 @@ class App extends Component {
</Link> </Link>
</Menu.Item> </Menu.Item>
); );
res.push(
<Menu.Item key="/providers">
<Link to="/providers">
{i18next.t("general:Providers")}
</Link>
</Menu.Item>
);


if (Setting.isLocalAdminUser(this.state.account)) { if (Setting.isLocalAdminUser(this.state.account)) {
res.push( 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="/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" 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="/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> </Switch>
</div> </div>
); );


+ 185
- 0
web/src/ProviderEditPage.js View File

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

+ 202
- 0
web/src/ProviderListPage.js View File

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

+ 56
- 0
web/src/backend/ProviderBackend.js View File

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

Loading…
Cancel
Save