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