| @@ -0,0 +1,109 @@ | |||
| // Copyright 2023 The casbin Authors. All Rights Reserved. | |||
| // | |||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||
| // you may not use this file except in compliance with the License. | |||
| // You may obtain a copy of the License at | |||
| // | |||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||
| // | |||
| // Unless required by applicable law or agreed to in writing, software | |||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
| // See the License for the specific language governing permissions and | |||
| // limitations under the License. | |||
| package controllers | |||
| import ( | |||
| "encoding/json" | |||
| "github.com/casbin/casibase/object" | |||
| ) | |||
| func (c *ApiController) GetGlobalTasks() { | |||
| tasks, err := object.GetGlobalTasks() | |||
| if err != nil { | |||
| c.ResponseError(err.Error()) | |||
| return | |||
| } | |||
| c.ResponseOk(object.GetMaskedTasks(tasks, true)) | |||
| } | |||
| func (c *ApiController) GetTasks() { | |||
| owner := "admin" | |||
| tasks, err := object.GetTasks(owner) | |||
| if err != nil { | |||
| c.ResponseError(err.Error()) | |||
| return | |||
| } | |||
| c.ResponseOk(object.GetMaskedTasks(tasks, true)) | |||
| } | |||
| func (c *ApiController) GetTask() { | |||
| id := c.Input().Get("id") | |||
| task, err := object.GetTask(id) | |||
| if err != nil { | |||
| c.ResponseError(err.Error()) | |||
| return | |||
| } | |||
| c.ResponseOk(object.GetMaskedTask(task, true)) | |||
| } | |||
| func (c *ApiController) UpdateTask() { | |||
| id := c.Input().Get("id") | |||
| var task object.Task | |||
| err := json.Unmarshal(c.Ctx.Input.RequestBody, &task) | |||
| if err != nil { | |||
| c.ResponseError(err.Error()) | |||
| return | |||
| } | |||
| success, err := object.UpdateTask(id, &task) | |||
| if err != nil { | |||
| c.ResponseError(err.Error()) | |||
| return | |||
| } | |||
| c.ResponseOk(success) | |||
| } | |||
| func (c *ApiController) AddTask() { | |||
| var task object.Task | |||
| err := json.Unmarshal(c.Ctx.Input.RequestBody, &task) | |||
| if err != nil { | |||
| c.ResponseError(err.Error()) | |||
| return | |||
| } | |||
| task.Owner = "admin" | |||
| success, err := object.AddTask(&task) | |||
| if err != nil { | |||
| c.ResponseError(err.Error()) | |||
| return | |||
| } | |||
| c.ResponseOk(success) | |||
| } | |||
| func (c *ApiController) DeleteTask() { | |||
| var task object.Task | |||
| err := json.Unmarshal(c.Ctx.Input.RequestBody, &task) | |||
| if err != nil { | |||
| c.ResponseError(err.Error()) | |||
| return | |||
| } | |||
| success, err := object.DeleteTask(&task) | |||
| if err != nil { | |||
| c.ResponseError(err.Error()) | |||
| return | |||
| } | |||
| c.ResponseOk(success) | |||
| } | |||
| @@ -141,4 +141,9 @@ func (a *Adapter) createTable() { | |||
| if err != nil { | |||
| panic(err) | |||
| } | |||
| err = a.engine.Sync2(new(Task)) | |||
| if err != nil { | |||
| panic(err) | |||
| } | |||
| } | |||
| @@ -27,9 +27,10 @@ type Task struct { | |||
| CreatedTime string `xorm:"varchar(100)" json:"createdTime"` | |||
| DisplayName string `xorm:"varchar(100)" json:"displayName"` | |||
| Application string `xorm:"varchar(100)" json:"application"` | |||
| Provider string `xorm:"varchar(100)" json:"provider"` | |||
| Application string `xorm:"varchar(100)" json:"application"` | |||
| Path string `xorm:"varchar(100)" json:"path"` | |||
| Log string `xorm:"mediumtext" json:"log"` | |||
| } | |||
| func GetMaskedTask(task *Task, isMaskEnabled bool) *Task { | |||
| @@ -98,6 +98,13 @@ func initAPI() { | |||
| beego.Router("/api/add-message", &controllers.ApiController{}, "POST:AddMessage") | |||
| beego.Router("/api/delete-message", &controllers.ApiController{}, "POST:DeleteMessage") | |||
| beego.Router("/api/get-global-tasks", &controllers.ApiController{}, "GET:GetGlobalTasks") | |||
| beego.Router("/api/get-tasks", &controllers.ApiController{}, "GET:GetTasks") | |||
| beego.Router("/api/get-task", &controllers.ApiController{}, "GET:GetTask") | |||
| beego.Router("/api/update-task", &controllers.ApiController{}, "POST:UpdateTask") | |||
| beego.Router("/api/add-task", &controllers.ApiController{}, "POST:AddTask") | |||
| beego.Router("/api/delete-task", &controllers.ApiController{}, "POST:DeleteTask") | |||
| 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") | |||
| @@ -45,6 +45,8 @@ import ChatEditPage from "./ChatEditPage"; | |||
| import ChatListPage from "./ChatListPage"; | |||
| import MessageListPage from "./MessageListPage"; | |||
| import MessageEditPage from "./MessageEditPage"; | |||
| import TaskListPage from "./TaskListPage"; | |||
| import TaskEditPage from "./TaskEditPage"; | |||
| import ChatPage from "./ChatPage"; | |||
| const {Header, Footer, Content} = Layout; | |||
| @@ -104,6 +106,8 @@ class App extends Component { | |||
| this.setState({selectedMenuKey: "/chats"}); | |||
| } else if (uri.includes("/messages")) { | |||
| this.setState({selectedMenuKey: "/messages"}); | |||
| } else if (uri.includes("/tasks")) { | |||
| this.setState({selectedMenuKey: "/tasks"}); | |||
| } else { | |||
| this.setState({selectedMenuKey: "null"}); | |||
| } | |||
| @@ -303,6 +307,9 @@ class App extends Component { | |||
| res.push(Setting.getItem(<Link to="/messages">{i18next.t("general:Messages")}</Link>, | |||
| "/messages")); | |||
| res.push(Setting.getItem(<Link to="/tasks">{i18next.t("general:Tasks")}</Link>, | |||
| "/tasks")); | |||
| if (Setting.isLocalAdminUser(this.state.account)) { | |||
| res.push(Setting.getItem( | |||
| <a target="_blank" rel="noreferrer" href={Setting.getMyProfileUrl(this.state.account).replace("/account", "/resources")}> | |||
| @@ -374,6 +381,8 @@ class App extends Component { | |||
| <Route exact path="/chats/:chatName" render={(props) => this.renderSigninIfNotSignedIn(<ChatEditPage account={this.state.account} {...props} />)} /> | |||
| <Route exact path="/messages" render={(props) => this.renderSigninIfNotSignedIn(<MessageListPage account={this.state.account} {...props} />)} /> | |||
| <Route exact path="/messages/:messageName" render={(props) => this.renderSigninIfNotSignedIn(<MessageEditPage account={this.state.account} {...props} />)} /> | |||
| <Route exact path="/tasks" render={(props) => this.renderSigninIfNotSignedIn(<TaskListPage account={this.state.account} {...props} />)} /> | |||
| <Route exact path="/tasks/:taskName" render={(props) => this.renderSigninIfNotSignedIn(<TaskEditPage account={this.state.account} {...props} />)} /> | |||
| <Route exact path="/chat" render={(props) => this.renderSigninIfNotSignedIn(<ChatPage account={this.state.account} {...props} />)} /> | |||
| </Switch> | |||
| ); | |||
| @@ -0,0 +1,187 @@ | |||
| // Copyright 2023 The casbin Authors. All Rights Reserved. | |||
| // | |||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||
| // you may not use this file except in compliance with the License. | |||
| // You may obtain a copy of the License at | |||
| // | |||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||
| // | |||
| // Unless required by applicable law or agreed to in writing, software | |||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
| // See the License for the specific language governing permissions and | |||
| // limitations under the License. | |||
| import React from "react"; | |||
| import {Button, Card, Col, Input, Row, Select} from "antd"; | |||
| import * as TaskBackend from "./backend/TaskBackend"; | |||
| import * as Setting from "./Setting"; | |||
| import i18next from "i18next"; | |||
| import * as ProviderBackend from "./backend/ProviderBackend"; | |||
| const {Option} = Select; | |||
| class TaskEditPage extends React.Component { | |||
| constructor(props) { | |||
| super(props); | |||
| this.state = { | |||
| classes: props, | |||
| taskName: props.match.params.taskName, | |||
| modelProviders: [], | |||
| task: null, | |||
| }; | |||
| } | |||
| UNSAFE_componentWillMount() { | |||
| this.getTask(); | |||
| this.getModelProviders(); | |||
| } | |||
| getTask() { | |||
| TaskBackend.getTask(this.props.account.name, this.state.taskName) | |||
| .then((res) => { | |||
| if (res.status === "ok") { | |||
| this.setState({ | |||
| task: res.data, | |||
| }); | |||
| } else { | |||
| Setting.showMessage("error", `Failed to get task: ${res.msg}`); | |||
| } | |||
| }); | |||
| } | |||
| getModelProviders() { | |||
| ProviderBackend.getProviders(this.props.account.name) | |||
| .then((res) => { | |||
| if (res.status === "ok") { | |||
| this.setState({ | |||
| modelProviders: res.data.filter(provider => provider.category === "Model"), | |||
| }); | |||
| } else { | |||
| Setting.showMessage("error", `Failed to get providers: ${res.msg}`); | |||
| } | |||
| }); | |||
| } | |||
| parseTaskField(key, value) { | |||
| if ([""].includes(key)) { | |||
| value = Setting.myParseInt(value); | |||
| } | |||
| return value; | |||
| } | |||
| updateTaskField(key, value) { | |||
| value = this.parseTaskField(key, value); | |||
| const task = this.state.task; | |||
| task[key] = value; | |||
| this.setState({ | |||
| task: task, | |||
| }); | |||
| } | |||
| renderTask() { | |||
| return ( | |||
| <Card size="small" title={ | |||
| <div> | |||
| {i18next.t("task:Edit Task")} | |||
| <Button type="primary" onClick={this.submitTaskEdit.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.task.name} onChange={e => { | |||
| this.updateTaskField("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.task.displayName} onChange={e => { | |||
| this.updateTaskField("displayName", e.target.value); | |||
| }} /> | |||
| </Col> | |||
| </Row> | |||
| <Row style={{marginTop: "20px"}} > | |||
| <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> | |||
| {i18next.t("store:Model provider")}: | |||
| </Col> | |||
| <Col span={22} > | |||
| <Select virtual={false} style={{width: "100%"}} value={this.state.task.provider} onChange={(value => {this.updateTaskField("provider", value);})} | |||
| options={this.state.modelProviders.map((provider) => Setting.getOption(`${provider.displayName} (${provider.name})`, `${provider.name}`)) | |||
| } /> | |||
| </Col> | |||
| </Row> | |||
| <Row style={{marginTop: "20px"}} > | |||
| <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> | |||
| {i18next.t("task:Application")}: | |||
| </Col> | |||
| <Col span={22} > | |||
| <Select virtual={false} style={{width: "100%"}} value={this.state.task.application} onChange={(value => {this.updateTaskField("application", value);})}> | |||
| { | |||
| [ | |||
| {id: "Docs-Polish", name: "Docs-Polish"}, | |||
| ].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>) | |||
| } | |||
| </Select> | |||
| </Col> | |||
| </Row> | |||
| <Row style={{marginTop: "20px"}} > | |||
| <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> | |||
| {i18next.t("task:Path")}: | |||
| </Col> | |||
| <Col span={22} > | |||
| <Input value={this.state.task.path} onChange={e => { | |||
| this.updateTaskField("path", e.target.value); | |||
| }} /> | |||
| </Col> | |||
| </Row> | |||
| </Card> | |||
| ); | |||
| } | |||
| submitTaskEdit() { | |||
| const task = Setting.deepCopy(this.state.task); | |||
| TaskBackend.updateTask(this.state.task.owner, this.state.taskName, task) | |||
| .then((res) => { | |||
| if (res.status === "ok") { | |||
| if (res.data) { | |||
| Setting.showMessage("success", "Successfully saved"); | |||
| this.setState({ | |||
| taskName: this.state.task.name, | |||
| }); | |||
| this.props.history.push(`/tasks/${this.state.task.name}`); | |||
| } else { | |||
| Setting.showMessage("error", "failed to save: server side failure"); | |||
| this.updateTaskField("name", this.state.taskName); | |||
| } | |||
| } else { | |||
| Setting.showMessage("error", `failed to save: ${res.msg}`); | |||
| } | |||
| }) | |||
| .catch(error => { | |||
| Setting.showMessage("error", `failed to save: ${error}`); | |||
| }); | |||
| } | |||
| render() { | |||
| return ( | |||
| <div> | |||
| { | |||
| this.state.task !== null ? this.renderTask() : null | |||
| } | |||
| <div style={{marginTop: "20px", marginLeft: "40px"}}> | |||
| <Button type="primary" size="large" onClick={this.submitTaskEdit.bind(this)}>{i18next.t("general:Save")}</Button> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| export default TaskEditPage; | |||
| @@ -0,0 +1,197 @@ | |||
| // Copyright 2023 The casbin Authors. All Rights Reserved. | |||
| // | |||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||
| // you may not use this file except in compliance with the License. | |||
| // You may obtain a copy of the License at | |||
| // | |||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||
| // | |||
| // Unless required by applicable law or agreed to in writing, software | |||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
| // See the License for the specific language governing permissions and | |||
| // limitations under the License. | |||
| import React from "react"; | |||
| import {Link} from "react-router-dom"; | |||
| import {Button, Popconfirm, Table} from "antd"; | |||
| import moment from "moment"; | |||
| import * as Setting from "./Setting"; | |||
| import * as TaskBackend from "./backend/TaskBackend"; | |||
| import i18next from "i18next"; | |||
| class TaskListPage extends React.Component { | |||
| constructor(props) { | |||
| super(props); | |||
| this.state = { | |||
| classes: props, | |||
| tasks: null, | |||
| }; | |||
| } | |||
| UNSAFE_componentWillMount() { | |||
| this.getTasks(); | |||
| } | |||
| getTasks() { | |||
| TaskBackend.getTasks(this.props.account.name) | |||
| .then((res) => { | |||
| if (res.status === "ok") { | |||
| this.setState({ | |||
| tasks: res.data, | |||
| }); | |||
| } else { | |||
| Setting.showMessage("error", `Failed to get tasks: ${res.msg}`); | |||
| } | |||
| }); | |||
| } | |||
| newTask() { | |||
| const randomName = Setting.getRandomName(); | |||
| return { | |||
| owner: this.props.account.name, | |||
| name: `task_${randomName}`, | |||
| createdTime: moment().format(), | |||
| displayName: `New Task - ${randomName}`, | |||
| provider: "provider_openai", | |||
| application: "Docs-Polish", | |||
| path: "F:/github_repos/casdoor-website", | |||
| }; | |||
| } | |||
| addTask() { | |||
| const newTask = this.newTask(); | |||
| TaskBackend.addTask(newTask) | |||
| .then((res) => { | |||
| if (res.status === "ok") { | |||
| Setting.showMessage("success", "Task added successfully"); | |||
| this.setState({ | |||
| tasks: Setting.prependRow(this.state.tasks, newTask), | |||
| }); | |||
| } else { | |||
| Setting.showMessage("error", `Failed to add task: ${res.msg}`); | |||
| } | |||
| }) | |||
| .catch(error => { | |||
| Setting.showMessage("error", `Task failed to add: ${error}`); | |||
| }); | |||
| } | |||
| deleteTask(i) { | |||
| TaskBackend.deleteTask(this.state.tasks[i]) | |||
| .then((res) => { | |||
| if (res.status === "ok") { | |||
| Setting.showMessage("success", "Task deleted successfully"); | |||
| this.setState({ | |||
| tasks: Setting.deleteRow(this.state.tasks, i), | |||
| }); | |||
| } else { | |||
| Setting.showMessage("error", `Task failed to delete: ${res.msg}`); | |||
| } | |||
| }) | |||
| .catch(error => { | |||
| Setting.showMessage("error", `Task failed to delete: ${error}`); | |||
| }); | |||
| } | |||
| renderTable(tasks) { | |||
| const columns = [ | |||
| { | |||
| title: i18next.t("general:Name"), | |||
| dataIndex: "name", | |||
| key: "name", | |||
| width: "160px", | |||
| sorter: (a, b) => a.name.localeCompare(b.name), | |||
| render: (text, record, index) => { | |||
| return ( | |||
| <Link to={`/tasks/${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:Model provider"), | |||
| dataIndex: "provider", | |||
| key: "provider", | |||
| width: "250px", | |||
| sorter: (a, b) => a.provider.localeCompare(b.provider), | |||
| render: (text, record, index) => { | |||
| return ( | |||
| <Link to={`/providers/${text}`}> | |||
| {text} | |||
| </Link> | |||
| ); | |||
| }, | |||
| }, | |||
| { | |||
| title: i18next.t("task:Application"), | |||
| dataIndex: "application", | |||
| key: "application", | |||
| width: "180px", | |||
| sorter: (a, b) => a.application.localeCompare(b.application), | |||
| }, | |||
| { | |||
| title: i18next.t("task:Path"), | |||
| dataIndex: "path", | |||
| key: "path", | |||
| // width: "160px", | |||
| sorter: (a, b) => a.path.localeCompare(b.path), | |||
| }, | |||
| { | |||
| title: i18next.t("general:Action"), | |||
| dataIndex: "action", | |||
| key: "action", | |||
| width: "180px", | |||
| render: (text, record, index) => { | |||
| return ( | |||
| <div> | |||
| <Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/tasks/${record.name}`)}>{i18next.t("general:Edit")}</Button> | |||
| <Popconfirm | |||
| title={`Sure to delete task: ${record.name} ?`} | |||
| onConfirm={() => this.deleteTask(index)} | |||
| okText="OK" | |||
| cancelText="Cancel" | |||
| > | |||
| <Button style={{marginBottom: "10px"}} type="primary" danger>{i18next.t("general:Delete")}</Button> | |||
| </Popconfirm> | |||
| </div> | |||
| ); | |||
| }, | |||
| }, | |||
| ]; | |||
| return ( | |||
| <div> | |||
| <Table scroll={{x: "max-content"}} columns={columns} dataSource={tasks} rowKey="name" size="middle" bordered pagination={{pageSize: 100}} | |||
| title={() => ( | |||
| <div> | |||
| {i18next.t("general:Tasks")} | |||
| <Button type="primary" size="small" onClick={this.addTask.bind(this)}>{i18next.t("general:Add")}</Button> | |||
| </div> | |||
| )} | |||
| loading={tasks === null} | |||
| /> | |||
| </div> | |||
| ); | |||
| } | |||
| render() { | |||
| return ( | |||
| <div> | |||
| { | |||
| this.renderTable(this.state.tasks) | |||
| } | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| export default TaskListPage; | |||
| @@ -0,0 +1,63 @@ | |||
| // Copyright 2023 The casbin Authors. All Rights Reserved. | |||
| // | |||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||
| // you may not use this file except in compliance with the License. | |||
| // You may obtain a copy of the License at | |||
| // | |||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||
| // | |||
| // Unless required by applicable law or agreed to in writing, software | |||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
| // See the License for the specific language governing permissions and | |||
| // limitations under the License. | |||
| import * as Setting from "../Setting"; | |||
| export function getGlobalTasks() { | |||
| return fetch(`${Setting.ServerUrl}/api/get-global-tasks`, { | |||
| method: "GET", | |||
| credentials: "include", | |||
| }).then(res => res.json()); | |||
| } | |||
| export function getTasks(owner) { | |||
| return fetch(`${Setting.ServerUrl}/api/get-tasks?owner=${owner}`, { | |||
| method: "GET", | |||
| credentials: "include", | |||
| }).then(res => res.json()); | |||
| } | |||
| export function getTask(owner, name) { | |||
| return fetch(`${Setting.ServerUrl}/api/get-task?id=${owner}/${encodeURIComponent(name)}`, { | |||
| method: "GET", | |||
| credentials: "include", | |||
| }).then(res => res.json()); | |||
| } | |||
| export function updateTask(owner, name, task) { | |||
| const newTask = Setting.deepCopy(task); | |||
| return fetch(`${Setting.ServerUrl}/api/update-task?id=${owner}/${encodeURIComponent(name)}`, { | |||
| method: "POST", | |||
| credentials: "include", | |||
| body: JSON.stringify(newTask), | |||
| }).then(res => res.json()); | |||
| } | |||
| export function addTask(task) { | |||
| const newTask = Setting.deepCopy(task); | |||
| return fetch(`${Setting.ServerUrl}/api/add-task`, { | |||
| method: "POST", | |||
| credentials: "include", | |||
| body: JSON.stringify(newTask), | |||
| }).then(res => res.json()); | |||
| } | |||
| export function deleteTask(task) { | |||
| const newTask = Setting.deepCopy(task); | |||
| return fetch(`${Setting.ServerUrl}/api/delete-task`, { | |||
| method: "POST", | |||
| credentials: "include", | |||
| body: JSON.stringify(newTask), | |||
| }).then(res => res.json()); | |||
| } | |||