| @@ -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() | |||
| } | |||
| @@ -99,4 +99,9 @@ func (a *Adapter) createTable() { | |||
| if err != nil { | |||
| panic(err) | |||
| } | |||
| err = a.engine.Sync2(new(Store)) | |||
| if err != nil { | |||
| panic(err) | |||
| } | |||
| } | |||
| @@ -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) | |||
| } | |||
| @@ -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") | |||
| } | |||
| @@ -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 { | |||
| </Link> | |||
| </Menu.Item> | |||
| ); | |||
| res.push( | |||
| <Menu.Item key="/stores"> | |||
| <Link to="/stores"> | |||
| {i18next.t("general:Stores")} | |||
| </Link> | |||
| </Menu.Item> | |||
| ); | |||
| return res; | |||
| } | |||
| @@ -299,6 +310,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="/stores" render={(props) => this.renderSigninIfNotSignedIn(<StoreListPage account={this.state.account} {...props} />)}/> | |||
| <Route exact path="/stores/:storeName" render={(props) => this.renderSigninIfNotSignedIn(<StoreEditPage account={this.state.account} {...props} />)}/> | |||
| </Switch> | |||
| </div> | |||
| ) | |||
| @@ -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 ( | |||
| <Card size="small" title={ | |||
| <div> | |||
| {i18next.t("store:Edit Store")} | |||
| <Button type="primary" onClick={this.submitStoreEdit.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.store.name} onChange={e => { | |||
| this.updateStoreField('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.store.displayName} onChange={e => { | |||
| this.updateStoreField('displayName', e.target.value); | |||
| }} /> | |||
| </Col> | |||
| </Row> | |||
| <Row style={{marginTop: '20px'}} > | |||
| <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> | |||
| {i18next.t("store:Vectorset")}: | |||
| </Col> | |||
| <Col span={22} > | |||
| <Select virtual={false} style={{width: '100%'}} value={this.state.store.vectorset} onChange={(value => {this.updateStoreField('vectorset', value);})}> | |||
| { | |||
| this.state.vectorsets?.map((vectorset, index) => <Option key={index} value={vectorset.name}>{vectorset.name}</Option>) | |||
| } | |||
| </Select> | |||
| </Col> | |||
| </Row> | |||
| <Row style={{marginTop: '20px'}} > | |||
| <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> | |||
| {i18next.t("store:Distance limit")}: | |||
| </Col> | |||
| <Col span={22} > | |||
| <InputNumber value={this.state.store.distanceLimit} onChange={value => { | |||
| this.updateStoreField('distanceLimit', value); | |||
| }} /> | |||
| </Col> | |||
| </Row> | |||
| <Row style={{marginTop: '20px'}} > | |||
| <Col style={{marginTop: '5px'}} span={(Setting.isMobile()) ? 22 : 2}> | |||
| {i18next.t("store:Words")}: | |||
| </Col> | |||
| <Col span={22} > | |||
| <VectorTable | |||
| title={i18next.t("store:Words")} | |||
| table={this.state.store.vectors} | |||
| store={this.state.store} | |||
| onUpdateTable={(value) => { this.updateStoreField('vectors', value)}} | |||
| /> | |||
| </Col> | |||
| </Row> | |||
| </Card> | |||
| ) | |||
| } | |||
| 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 ( | |||
| <div> | |||
| <Row style={{width: "100%"}}> | |||
| <Col span={1}> | |||
| </Col> | |||
| <Col span={22}> | |||
| { | |||
| this.state.store !== null ? this.renderStore() : 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.submitStoreEdit.bind(this)}>{i18next.t("general:Save")}</Button> | |||
| </Col> | |||
| </Row> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| export default StoreEditPage; | |||
| @@ -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 ( | |||
| <Link to={`/stores/${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("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 ( | |||
| <div> | |||
| <Button style={{marginTop: '10px', marginBottom: '10px', marginRight: '10px'}} onClick={() => Setting.openLink(`/stores/${record.name}/graph`)}>{i18next.t("general:Result")}</Button> | |||
| <Button style={{marginBottom: '10px', marginRight: '10px'}} onClick={() => Setting.downloadXlsx(record)}>{i18next.t("general:Download")}</Button> | |||
| <Button style={{marginBottom: '10px', marginRight: '10px'}} type="primary" onClick={() => this.props.history.push(`/stores/${record.name}`)}>{i18next.t("general:Edit")}</Button> | |||
| <Popconfirm | |||
| title={`Sure to delete store: ${record.name} ?`} | |||
| onConfirm={() => this.deleteStore(index)} | |||
| okText="OK" | |||
| cancelText="Cancel" | |||
| > | |||
| <Button style={{marginBottom: '10px'}} type="danger">{i18next.t("general:Delete")}</Button> | |||
| </Popconfirm> | |||
| </div> | |||
| ) | |||
| } | |||
| }, | |||
| ]; | |||
| return ( | |||
| <div> | |||
| <Table columns={columns} dataSource={stores} rowKey="name" size="middle" bordered pagination={{pageSize: 100}} | |||
| title={() => ( | |||
| <div> | |||
| {i18next.t("general:Stores")} | |||
| <Button type="primary" size="small" onClick={this.addStore.bind(this)}>{i18next.t("general:Add")}</Button> | |||
| </div> | |||
| )} | |||
| loading={stores === null} | |||
| /> | |||
| </div> | |||
| ); | |||
| } | |||
| render() { | |||
| return ( | |||
| <div> | |||
| <Row style={{width: "100%"}}> | |||
| <Col span={1}> | |||
| </Col> | |||
| <Col span={22}> | |||
| { | |||
| this.renderTable(this.state.stores) | |||
| } | |||
| </Col> | |||
| <Col span={1}> | |||
| </Col> | |||
| </Row> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| export default StoreListPage; | |||
| @@ -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()); | |||
| } | |||