Browse Source

Add store pages.

HEAD
Yang Luo 3 years ago
parent
commit
f5e9e7a3d3
8 changed files with 582 additions and 0 deletions
  1. +61
    -0
      controllers/store.go
  2. +5
    -0
      object/adapter.go
  3. +99
    -0
      object/store.go
  4. +7
    -0
      routers/router.go
  5. +13
    -0
      web/src/App.js
  6. +166
    -0
      web/src/StoreEditPage.js
  7. +182
    -0
      web/src/StoreListPage.js
  8. +49
    -0
      web/src/backend/StoreBackend.js

+ 61
- 0
controllers/store.go View File

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

+ 5
- 0
object/adapter.go View File

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

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

+ 99
- 0
object/store.go View File

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

+ 7
- 0
routers/router.go View File

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

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

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


+ 166
- 0
web/src/StoreEditPage.js View File

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

+ 182
- 0
web/src/StoreListPage.js View File

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

+ 49
- 0
web/src/backend/StoreBackend.js View File

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

Loading…
Cancel
Save