diff --git a/controllers/task.go b/controllers/task.go new file mode 100644 index 0000000..f32a153 --- /dev/null +++ b/controllers/task.go @@ -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) +} diff --git a/object/adapter.go b/object/adapter.go index 752d847..fe21f31 100644 --- a/object/adapter.go +++ b/object/adapter.go @@ -141,4 +141,9 @@ func (a *Adapter) createTable() { if err != nil { panic(err) } + + err = a.engine.Sync2(new(Task)) + if err != nil { + panic(err) + } } diff --git a/object/task.go b/object/task.go index 8924d40..3602baf 100644 --- a/object/task.go +++ b/object/task.go @@ -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 { diff --git a/routers/router.go b/routers/router.go index 40093ca..423dbc9 100644 --- a/routers/router.go +++ b/routers/router.go @@ -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") diff --git a/web/src/App.js b/web/src/App.js index bbe2a68..890cd2c 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -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({i18next.t("general:Messages")}, "/messages")); + res.push(Setting.getItem({i18next.t("general:Tasks")}, + "/tasks")); + if (Setting.isLocalAdminUser(this.state.account)) { res.push(Setting.getItem( @@ -374,6 +381,8 @@ class App extends Component { this.renderSigninIfNotSignedIn()} /> this.renderSigninIfNotSignedIn()} /> this.renderSigninIfNotSignedIn()} /> + this.renderSigninIfNotSignedIn()} /> + this.renderSigninIfNotSignedIn()} /> this.renderSigninIfNotSignedIn()} /> ); diff --git a/web/src/TaskEditPage.js b/web/src/TaskEditPage.js new file mode 100644 index 0000000..3d00ae3 --- /dev/null +++ b/web/src/TaskEditPage.js @@ -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 ( + + {i18next.t("task:Edit Task")}     + + + } style={{marginLeft: "5px"}} type="inner"> + + + {i18next.t("general:Name")}: + + + { + this.updateTaskField("name", e.target.value); + }} /> + + + + + {i18next.t("general:Display name")}: + + + { + this.updateTaskField("displayName", e.target.value); + }} /> + + + + + {i18next.t("store:Model provider")}: + + + {this.updateTaskField("application", value);})}> + { + [ + {id: "Docs-Polish", name: "Docs-Polish"}, + ].map((item, index) => ) + } + + + + + + {i18next.t("task:Path")}: + + + { + this.updateTaskField("path", e.target.value); + }} /> + + + + ); + } + + 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 ( +
+ { + this.state.task !== null ? this.renderTask() : null + } +
+ +
+
+ ); + } +} + +export default TaskEditPage; diff --git a/web/src/TaskListPage.js b/web/src/TaskListPage.js new file mode 100644 index 0000000..d4951ba --- /dev/null +++ b/web/src/TaskListPage.js @@ -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 ( + + {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:Model provider"), + dataIndex: "provider", + key: "provider", + width: "250px", + sorter: (a, b) => a.provider.localeCompare(b.provider), + render: (text, record, index) => { + return ( + + {text} + + ); + }, + }, + { + 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 ( +
+ + this.deleteTask(index)} + okText="OK" + cancelText="Cancel" + > + + +
+ ); + }, + }, + ]; + + return ( +
+ ( +
+ {i18next.t("general:Tasks")}     + +
+ )} + loading={tasks === null} + /> + + ); + } + + render() { + return ( +
+ { + this.renderTable(this.state.tasks) + } +
+ ); + } +} + +export default TaskListPage; diff --git a/web/src/backend/TaskBackend.js b/web/src/backend/TaskBackend.js new file mode 100644 index 0000000..c0e9364 --- /dev/null +++ b/web/src/backend/TaskBackend.js @@ -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()); +}