From f5e9e7a3d3c852580790f345ea7e4cc1164cfc32 Mon Sep 17 00:00:00 2001 From: Yang Luo Date: Sat, 16 Jul 2022 14:32:14 +0800 Subject: [PATCH] Add store pages. --- controllers/store.go | 61 +++++++++++ object/adapter.go | 5 + object/store.go | 99 +++++++++++++++++ routers/router.go | 7 ++ web/src/App.js | 13 +++ web/src/StoreEditPage.js | 166 +++++++++++++++++++++++++++++ web/src/StoreListPage.js | 182 ++++++++++++++++++++++++++++++++ web/src/backend/StoreBackend.js | 49 +++++++++ 8 files changed, 582 insertions(+) create mode 100644 controllers/store.go create mode 100644 object/store.go create mode 100644 web/src/StoreEditPage.js create mode 100644 web/src/StoreListPage.js create mode 100644 web/src/backend/StoreBackend.js diff --git a/controllers/store.go b/controllers/store.go new file mode 100644 index 0000000..9a8bf11 --- /dev/null +++ b/controllers/store.go @@ -0,0 +1,61 @@ +package controllers + +import ( + "encoding/json" + + "github.com/casbin/casbase/object" +) + +func (c *ApiController) GetGlobalStores() { + c.Data["json"] = object.GetGlobalStores() + c.ServeJSON() +} + +func (c *ApiController) GetStores() { + owner := c.Input().Get("owner") + + c.Data["json"] = object.GetStores(owner) + c.ServeJSON() +} + +func (c *ApiController) GetStore() { + id := c.Input().Get("id") + + c.Data["json"] = object.GetStore(id) + c.ServeJSON() +} + +func (c *ApiController) UpdateStore() { + id := c.Input().Get("id") + + var store object.Store + err := json.Unmarshal(c.Ctx.Input.RequestBody, &store) + if err != nil { + panic(err) + } + + c.Data["json"] = object.UpdateStore(id, &store) + c.ServeJSON() +} + +func (c *ApiController) AddStore() { + var store object.Store + err := json.Unmarshal(c.Ctx.Input.RequestBody, &store) + if err != nil { + panic(err) + } + + c.Data["json"] = object.AddStore(&store) + c.ServeJSON() +} + +func (c *ApiController) DeleteStore() { + var store object.Store + err := json.Unmarshal(c.Ctx.Input.RequestBody, &store) + if err != nil { + panic(err) + } + + c.Data["json"] = object.DeleteStore(&store) + c.ServeJSON() +} diff --git a/object/adapter.go b/object/adapter.go index 3d9a7e2..693a179 100644 --- a/object/adapter.go +++ b/object/adapter.go @@ -99,4 +99,9 @@ func (a *Adapter) createTable() { if err != nil { panic(err) } + + err = a.engine.Sync2(new(Store)) + if err != nil { + panic(err) + } } diff --git a/object/store.go b/object/store.go new file mode 100644 index 0000000..5701b61 --- /dev/null +++ b/object/store.go @@ -0,0 +1,99 @@ +package object + +import ( + "fmt" + + "github.com/casbin/casbase/util" + "xorm.io/core" +) + +type Folder struct { + Name string `xorm:"varchar(100)" json:"name"` + Desc string `xorm:"mediumtext" json:"desc"` + Children []*Folder `xorm:"varchar(1000)" json:"children"` +} + +type Store 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"` + + Folders []*Folder `xorm:"mediumtext" json:"folders"` +} + +func GetGlobalStores() []*Store { + stores := []*Store{} + err := adapter.engine.Asc("owner").Desc("created_time").Find(&stores) + if err != nil { + panic(err) + } + + return stores +} + +func GetStores(owner string) []*Store { + stores := []*Store{} + err := adapter.engine.Desc("created_time").Find(&stores, &Store{Owner: owner}) + if err != nil { + panic(err) + } + + return stores +} + +func getStore(owner string, name string) *Store { + store := Store{Owner: owner, Name: name} + existed, err := adapter.engine.Get(&store) + if err != nil { + panic(err) + } + + if existed { + return &store + } else { + return nil + } +} + +func GetStore(id string) *Store { + owner, name := util.GetOwnerAndNameFromId(id) + return getStore(owner, name) +} + +func UpdateStore(id string, store *Store) bool { + owner, name := util.GetOwnerAndNameFromId(id) + if getStore(owner, name) == nil { + return false + } + + _, err := adapter.engine.ID(core.PK{owner, name}).AllCols().Update(store) + if err != nil { + panic(err) + } + + //return affected != 0 + return true +} + +func AddStore(store *Store) bool { + affected, err := adapter.engine.Insert(store) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func DeleteStore(store *Store) bool { + affected, err := adapter.engine.ID(core.PK{store.Owner, store.Name}).Delete(&Store{}) + if err != nil { + panic(err) + } + + return affected != 0 +} + +func (store *Store) GetId() string { + return fmt.Sprintf("%s/%s", store.Owner, store.Name) +} diff --git a/routers/router.go b/routers/router.go index 9da051c..83f7e83 100644 --- a/routers/router.go +++ b/routers/router.go @@ -44,4 +44,11 @@ func initAPI() { beego.Router("/api/update-video", &controllers.ApiController{}, "POST:UpdateVideo") beego.Router("/api/add-video", &controllers.ApiController{}, "POST:AddVideo") beego.Router("/api/delete-video", &controllers.ApiController{}, "POST:DeleteVideo") + + beego.Router("/api/get-global-stores", &controllers.ApiController{}, "GET:GetGlobalStores") + beego.Router("/api/get-stores", &controllers.ApiController{}, "GET:GetStores") + beego.Router("/api/get-store", &controllers.ApiController{}, "GET:GetStore") + beego.Router("/api/update-store", &controllers.ApiController{}, "POST:UpdateStore") + beego.Router("/api/add-store", &controllers.ApiController{}, "POST:AddStore") + beego.Router("/api/delete-store", &controllers.ApiController{}, "POST:DeleteStore") } diff --git a/web/src/App.js b/web/src/App.js index 68cc3fc..49b75cf 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -15,6 +15,8 @@ import VectorsetListPage from "./VectorsetListPage"; import VectorsetEditPage from "./VectorsetEditPage"; import VideoListPage from "./VideoListPage"; import VideoEditPage from "./VideoEditPage"; +import StoreListPage from "./StoreListPage"; +import StoreEditPage from "./StoreEditPage"; import SigninPage from "./SigninPage"; import i18next from "i18next"; import SelectLanguageBox from "./SelectLanguageBox"; @@ -62,6 +64,8 @@ class App extends Component { this.setState({ selectedMenuKey: '/vectorsets' }); } else if (uri.includes('/videos')) { this.setState({ selectedMenuKey: '/videos' }); + } else if (uri.includes('/stores')) { + this.setState({ selectedMenuKey: '/stores' }); } else { this.setState({selectedMenuKey: 'null'}); } @@ -242,6 +246,13 @@ class App extends Component { ); + res.push( + + + {i18next.t("general:Stores")} + + + ); return res; } @@ -299,6 +310,8 @@ class App extends Component { this.renderSigninIfNotSignedIn()}/> this.renderSigninIfNotSignedIn()}/> this.renderSigninIfNotSignedIn()}/> + this.renderSigninIfNotSignedIn()}/> + this.renderSigninIfNotSignedIn()}/> ) diff --git a/web/src/StoreEditPage.js b/web/src/StoreEditPage.js new file mode 100644 index 0000000..3118160 --- /dev/null +++ b/web/src/StoreEditPage.js @@ -0,0 +1,166 @@ +import React from "react"; +import {Button, Card, Col, Input, InputNumber, Row, Select} from 'antd'; +import * as StoreBackend from "./backend/StoreBackend"; +import * as Setting from "./Setting"; +import i18next from "i18next"; +import VectorTable from "./VectorTable"; + +const { Option } = Select; + +class StoreEditPage extends React.Component { + constructor(props) { + super(props); + this.state = { + classes: props, + storeName: props.match.params.storeName, + store: null, + vectorsets: null, + matchLoading: false, + }; + } + + componentWillMount() { + this.getStore(); + this.getVectorsets(); + } + + getStore() { + StoreBackend.getStore(this.props.account.name, this.state.storeName) + .then((store) => { + this.setState({ + store: store, + }); + }); + } + + parseStoreField(key, value) { + if (["score"].includes(key)) { + value = Setting.myParseInt(value); + } + return value; + } + + updateStoreField(key, value) { + value = this.parseStoreField(key, value); + + let store = this.state.store; + store[key] = value; + this.setState({ + store: store, + }); + } + + renderStore() { + return ( + + {i18next.t("store:Edit Store")}     + + + } style={{marginLeft: '5px'}} type="inner"> + + + {i18next.t("general:Name")}: + + + { + this.updateStoreField('name', e.target.value); + }} /> + + + + + {i18next.t("general:Display name")}: + + + { + this.updateStoreField('displayName', e.target.value); + }} /> + + + + + {i18next.t("store:Vectorset")}: + + + + + + + + {i18next.t("store:Distance limit")}: + + + { + this.updateStoreField('distanceLimit', value); + }} /> + + + + + {i18next.t("store:Words")}: + + + { this.updateStoreField('vectors', value)}} + /> + + + + ) + } + + submitStoreEdit() { + let store = Setting.deepCopy(this.state.store); + StoreBackend.updateStore(this.state.store.owner, this.state.storeName, store) + .then((res) => { + if (res) { + Setting.showMessage("success", `Successfully saved`); + this.setState({ + storeName: this.state.store.name, + }); + this.props.history.push(`/stores/${this.state.store.name}`); + } else { + Setting.showMessage("error", `failed to save: server side failure`); + this.updateStoreField('name', this.state.storeName); + } + }) + .catch(error => { + Setting.showMessage("error", `failed to save: ${error}`); + }); + } + + render() { + return ( +
+ + + + + { + this.state.store !== null ? this.renderStore() : null + } + + + + + + + + + + + +
+ ); + } +} + +export default StoreEditPage; diff --git a/web/src/StoreListPage.js b/web/src/StoreListPage.js new file mode 100644 index 0000000..9775561 --- /dev/null +++ b/web/src/StoreListPage.js @@ -0,0 +1,182 @@ +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 StoreBackend from "./backend/StoreBackend"; +import i18next from "i18next"; + +class StoreListPage extends React.Component { + constructor(props) { + super(props); + this.state = { + classes: props, + stores: null, + }; + } + + componentWillMount() { + this.getStores(); + } + + getStores() { + StoreBackend.getStores(this.props.account.name) + .then((res) => { + this.setState({ + stores: res, + }); + }); + } + + newStore() { + return { + owner: this.props.account.name, + name: `store_${this.state.stores.length}`, + createdTime: moment().format(), + displayName: `Store ${this.state.stores.length}`, + children: [], + } + } + + addStore() { + const newStore = this.newStore(); + StoreBackend.addStore(newStore) + .then((res) => { + Setting.showMessage("success", `Store added successfully`); + this.setState({ + stores: Setting.prependRow(this.state.stores, newStore), + }); + } + ) + .catch(error => { + Setting.showMessage("error", `Store failed to add: ${error}`); + }); + } + + deleteStore(i) { + StoreBackend.deleteStore(this.state.stores[i]) + .then((res) => { + Setting.showMessage("success", `Store deleted successfully`); + this.setState({ + stores: Setting.deleteRow(this.state.stores, i), + }); + } + ) + .catch(error => { + Setting.showMessage("error", `Store failed to delete: ${error}`); + }); + } + + renderTable(stores) { + const columns = [ + { + title: i18next.t("general:Name"), + dataIndex: 'name', + key: 'name', + width: '120px', + 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("store:Words"), + dataIndex: 'vectors', + key: 'vectors', + // width: '120px', + sorter: (a, b) => a.vectors.localeCompare(b.vectors), + render: (text, record, index) => { + return Setting.getTags(text); + } + }, + // { + // title: i18next.t("store:All words"), + // dataIndex: 'allWords', + // key: 'allWords', + // width: '140px', + // sorter: (a, b) => a.allWords - b.allWords, + // render: (text, record, index) => { + // return record.vectors.length; + // } + // }, + // { + // title: i18next.t("store:Valid words"), + // dataIndex: 'validWords', + // key: 'validWords', + // width: '140px', + // sorter: (a, b) => a.validWords - b.validWords, + // render: (text, record, index) => { + // return record.vectors.filter(vector => vector.data.length !== 0).length; + // } + // }, + { + title: i18next.t("general:Action"), + dataIndex: 'action', + key: 'action', + width: '80px', + render: (text, record, index) => { + return ( +
+ + + + this.deleteStore(index)} + okText="OK" + cancelText="Cancel" + > + + +
+ ) + } + }, + ]; + + return ( +
+ ( +
+ {i18next.t("general:Stores")}     + +
+ )} + loading={stores === null} + /> + + ); + } + + render() { + return ( +
+ +
+ + + { + this.renderTable(this.state.stores) + } + + + + + + ); + } +} + +export default StoreListPage; diff --git a/web/src/backend/StoreBackend.js b/web/src/backend/StoreBackend.js new file mode 100644 index 0000000..af11dc3 --- /dev/null +++ b/web/src/backend/StoreBackend.js @@ -0,0 +1,49 @@ +import * as Setting from "../Setting"; + +export function getGlobalStores() { + return fetch(`${Setting.ServerUrl}/api/get-global-stores`, { + method: "GET", + credentials: "include" + }).then(res => res.json()); +} + +export function getStores(owner) { + return fetch(`${Setting.ServerUrl}/api/get-stores?owner=${owner}`, { + method: "GET", + credentials: "include" + }).then(res => res.json()); +} + +export function getStore(owner, name) { + return fetch(`${Setting.ServerUrl}/api/get-store?id=${owner}/${encodeURIComponent(name)}`, { + method: "GET", + credentials: "include" + }).then(res => res.json()); +} + +export function updateStore(owner, name, store) { + let newStore = Setting.deepCopy(store); + return fetch(`${Setting.ServerUrl}/api/update-store?id=${owner}/${encodeURIComponent(name)}`, { + method: 'POST', + credentials: 'include', + body: JSON.stringify(newStore), + }).then(res => res.json()); +} + +export function addStore(store) { + let newStore = Setting.deepCopy(store); + return fetch(`${Setting.ServerUrl}/api/add-store`, { + method: 'POST', + credentials: 'include', + body: JSON.stringify(newStore), + }).then(res => res.json()); +} + +export function deleteStore(store) { + let newStore = Setting.deepCopy(store); + return fetch(`${Setting.ServerUrl}/api/delete-store`, { + method: 'POST', + credentials: 'include', + body: JSON.stringify(newStore), + }).then(res => res.json()); +}