From ac76b1fb9b577554dccc14fde53a2806df939086 Mon Sep 17 00:00:00 2001 From: Yang Luo Date: Sun, 16 Jul 2023 02:55:36 +0800 Subject: [PATCH] Add provider pages --- controllers/provider.go | 94 ++++++++++++++ object/adapter.go | 5 + object/provider.go | 101 +++++++++++++++ routers/router.go | 7 + web/src/App.js | 13 ++ web/src/ProviderEditPage.js | 185 ++++++++++++++++++++++++++ web/src/ProviderListPage.js | 202 +++++++++++++++++++++++++++++ web/src/backend/ProviderBackend.js | 56 ++++++++ 8 files changed, 663 insertions(+) create mode 100644 controllers/provider.go create mode 100644 object/provider.go create mode 100644 web/src/ProviderEditPage.js create mode 100644 web/src/ProviderListPage.js create mode 100644 web/src/backend/ProviderBackend.js diff --git a/controllers/provider.go b/controllers/provider.go new file mode 100644 index 0000000..1608477 --- /dev/null +++ b/controllers/provider.go @@ -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) +} diff --git a/object/adapter.go b/object/adapter.go index f5ada4e..ed375be 100644 --- a/object/adapter.go +++ b/object/adapter.go @@ -108,4 +108,9 @@ func (a *Adapter) createTable() { if err != nil { panic(err) } + + err = a.engine.Sync2(new(Provider)) + if err != nil { + panic(err) + } } diff --git a/object/provider.go b/object/provider.go new file mode 100644 index 0000000..e65e064 --- /dev/null +++ b/object/provider.go @@ -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) +} diff --git a/routers/router.go b/routers/router.go index 2815b4b..46e1ddf 100644 --- a/routers/router.go +++ b/routers/router.go @@ -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") diff --git a/web/src/App.js b/web/src/App.js index ec195dd..5846d17 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -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 { ); + res.push( + + + {i18next.t("general:Providers")} + + + ); if (Setting.isLocalAdminUser(this.state.account)) { res.push( @@ -363,6 +374,8 @@ class App extends Component { this.renderSigninIfNotSignedIn()} /> this.renderSigninIfNotSignedIn()} /> this.renderSigninIfNotSignedIn()} /> + this.renderSigninIfNotSignedIn()} /> + this.renderSigninIfNotSignedIn()} /> ); diff --git a/web/src/ProviderEditPage.js b/web/src/ProviderEditPage.js new file mode 100644 index 0000000..aeb8a55 --- /dev/null +++ b/web/src/ProviderEditPage.js @@ -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 ( + + {i18next.t("provider:Edit Provider")}     + + + } style={{marginLeft: "5px"}} type="inner"> + + + {i18next.t("general:Name")}: + + + { + this.updateProviderField("name", e.target.value); + }} /> + + + + + {i18next.t("general:Display name")}: + + + { + this.updateProviderField("displayName", e.target.value); + }} /> + + + + + {i18next.t("provider:Category")}: + + + + + + + + {i18next.t("provider:Type")}: + + + + + + + + {i18next.t("provider:Secret key")}: + + + { + this.updateProviderField("clientSecret", e.target.value); + }} /> + + + + + {i18next.t("general:Provider URL")}: + + + } value={this.state.provider.providerUrl} onChange={e => { + this.updateProviderField("providerUrl", e.target.value); + }} /> + + + + ); + } + + 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 ( +
+ + + + + { + this.state.provider !== null ? this.renderProvider() : null + } + + + + + + + + + + + +
+ ); + } +} + +export default ProviderEditPage; diff --git a/web/src/ProviderListPage.js b/web/src/ProviderListPage.js new file mode 100644 index 0000000..fcf28fe --- /dev/null +++ b/web/src/ProviderListPage.js @@ -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 ( + + {text} + + ); + }, + }, + { + 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 ( + + { + Setting.getShortText(text) + } + + ); + }, + }, + { + title: i18next.t("general:Action"), + dataIndex: "action", + key: "action", + width: "180px", + render: (text, record, index) => { + return ( +
+ + this.deleteProvider(index)} + okText="OK" + cancelText="Cancel" + > + + +
+ ); + }, + }, + ]; + + return ( +
+ ( +
+ {i18next.t("general:Providers")}     + +
+ )} + loading={providers === null} + /> + + ); + } + + render() { + return ( +
+ +
+ + + { + this.renderTable(this.state.providers) + } + + + + + + ); + } +} + +export default ProviderListPage; diff --git a/web/src/backend/ProviderBackend.js b/web/src/backend/ProviderBackend.js new file mode 100644 index 0000000..a72abea --- /dev/null +++ b/web/src/backend/ProviderBackend.js @@ -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()); +}