Compare commits

...

11 Commits

Author SHA1 Message Date
Calcium-Ion
16725d1226 Merge pull request #683 from iszcz/new512
Update channel-test.go
2024-12-31 20:22:57 +08:00
CalciumIon
e6ea5e59c0 fix: error page size opts 2024-12-31 15:51:15 +08:00
CalciumIon
4f196a62e1 feat: implement pagination and total count for redemptions API #386
- Updated GetAllRedemptions and SearchRedemptions functions to return total count along with paginated results.
- Modified API endpoints to accept page size as a parameter, enhancing flexibility in data retrieval.
- Adjusted RedemptionsTable component to support pagination and display total count, improving user experience.
- Ensured consistent handling of pagination across related components, including LogsTable and UsersTable.
2024-12-31 15:28:25 +08:00
CalciumIon
014fb7edab feat: enhance user search functionality with pagination support
- Updated SearchUsers function to include pagination parameters (startIdx and num) for improved user search results.
- Modified API response structure to return paginated data, including total user count and current page information.
- Adjusted UsersTable component to handle pagination and search parameters, ensuring a seamless user experience.
- Added internationalization support for new search functionality in the UI.
2024-12-31 15:02:59 +08:00
CalciumIon
be0b2f6a64 feat: enhance user management and pagination features #518
- Updated GetAllUsers function to return total user count along with paginated results, improving data handling in user retrieval.
- Modified GetAllUsers API endpoint to accept page size as a parameter, allowing for dynamic pagination.
- Enhanced UsersTable component to support customizable page sizes and improved pagination logic.
- Added error handling for empty username and password in AddUser component.
- Updated LogsTable component to display pagination information in a user-friendly format.
2024-12-31 14:52:55 +08:00
iszcz
687f07bc10 Update channel-test.go 2024-12-31 12:49:13 +08:00
CalciumIon
a7e5f1e509 fix: try to fix pgsql #682 2024-12-31 02:10:19 +08:00
CalciumIon
87d5e286d5 fix: try to fix pgsql #682 2024-12-31 02:06:30 +08:00
CalciumIon
b4f17543cb fix redis 2024-12-30 22:05:41 +08:00
CalciumIon
1eb706de7a docs: update README 2024-12-30 20:56:54 +08:00
CalciumIon
d13d81baba refactor: update group handling and rendering logic
- Changed the structure of usableGroups in GetUserGroups to store additional information (ratio and description) for each group.
- Introduced a new renderRatio function to visually represent group ratios with color coding.
- Updated the Playground and EditToken components to utilize the new group structure and rendering options.
- Enhanced the renderGroupOption function for better UI representation of group options.
- Fixed minor comments and improved code readability.
2024-12-30 19:51:00 +08:00
21 changed files with 470 additions and 234 deletions

View File

@@ -83,6 +83,7 @@ You can add custom models gpt-4-gizmo-* in channels. These are third-party model
- `COHERE_SAFETY_SETTING`: Cohere model [safety settings](https://docs.cohere.com/docs/safety-modes#overview), options: `NONE`, `CONTEXTUAL`, `STRICT`, default `NONE`
- `GEMINI_VISION_MAX_IMAGE_NUM`: Gemini model maximum image number, default `16`, set to `-1` to disable
- `MAX_FILE_DOWNLOAD_MB`: Maximum file download size in MB, default `20`
- `CRYPTO_SECRET`: Encryption key for encrypting database content
## Deployment
> [!TIP]
@@ -93,6 +94,10 @@ You can add custom models gpt-4-gizmo-* in channels. These are third-party model
> docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR
> ```
### Multi-Server Deployment
- Must set `SESSION_SECRET` environment variable, otherwise login state will not be consistent across multiple servers.
- If using a public Redis, must set `CRYPTO_SECRET` environment variable, otherwise Redis content will not be able to be obtained in multi-server deployment.
### Requirements
- Local database (default): SQLite (Docker deployment must mount `/data` directory)
- Remote database: MySQL >= 5.7.8, PgSQL >= 9.6

View File

@@ -101,7 +101,7 @@
### 多机部署
- 必须设置环境变量 `SESSION_SECRET`,否则会导致多机部署时登录状态不一致。
- 如果公用Redis必须设置 `CRYPTO_SECRET`否则会导致多机部署时Redis内容无法解密
- 如果公用Redis必须设置 `CRYPTO_SECRET`否则会导致多机部署时Redis内容无法获取
### 部署要求
- 本地数据库默认SQLiteDocker 部署默认使用 SQLite必须挂载 `/data` 目录到宿主机)

View File

@@ -5,7 +5,6 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/bytedance/gopkg/util/gopool"
"io"
"math"
"net/http"
@@ -24,6 +23,8 @@ import (
"sync"
"time"
"github.com/bytedance/gopkg/util/gopool"
"github.com/gin-gonic/gin"
)
@@ -32,6 +33,9 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
if channel.Type == common.ChannelTypeMidjourney {
return errors.New("midjourney channel test is not supported"), nil
}
if channel.Type == common.ChannelTypeMidjourneyPlus {
return errors.New("midjourney plus channel test is not supported!!!"), nil
}
if channel.Type == common.ChannelTypeSunoAPI {
return errors.New("suno channel test is not supported"), nil
}

View File

@@ -20,15 +20,18 @@ func GetGroups(c *gin.Context) {
}
func GetUserGroups(c *gin.Context) {
usableGroups := make(map[string]string)
usableGroups := make(map[string]map[string]interface{})
userGroup := ""
userId := c.GetInt("id")
userGroup, _ = model.GetUserGroup(userId, false)
for groupName, _ := range setting.GetGroupRatioCopy() {
for groupName, ratio := range setting.GetGroupRatioCopy() {
// UserUsableGroups contains the groups that the user can use
userUsableGroups := setting.GetUserUsableGroups(userGroup)
if _, ok := userUsableGroups[groupName]; ok {
usableGroups[groupName] = userUsableGroups[groupName]
if desc, ok := userUsableGroups[groupName]; ok {
usableGroups[groupName] = map[string]interface{}{
"ratio": ratio,
"desc": desc,
}
}
}
c.JSON(http.StatusOK, gin.H{

View File

@@ -1,19 +1,24 @@
package controller
import (
"github.com/gin-gonic/gin"
"net/http"
"one-api/common"
"one-api/model"
"strconv"
"github.com/gin-gonic/gin"
)
func GetAllRedemptions(c *gin.Context) {
p, _ := strconv.Atoi(c.Query("p"))
pageSize, _ := strconv.Atoi(c.Query("page_size"))
if p < 0 {
p = 0
}
redemptions, err := model.GetAllRedemptions(p*common.ItemsPerPage, common.ItemsPerPage)
if pageSize < 1 {
pageSize = common.ItemsPerPage
}
redemptions, total, err := model.GetAllRedemptions((p-1)*pageSize, pageSize)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
@@ -24,14 +29,27 @@ func GetAllRedemptions(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": redemptions,
"data": gin.H{
"items": redemptions,
"total": total,
"page": p,
"page_size": pageSize,
},
})
return
}
func SearchRedemptions(c *gin.Context) {
keyword := c.Query("keyword")
redemptions, err := model.SearchRedemptions(keyword)
p, _ := strconv.Atoi(c.Query("p"))
pageSize, _ := strconv.Atoi(c.Query("page_size"))
if p < 0 {
p = 0
}
if pageSize < 1 {
pageSize = common.ItemsPerPage
}
redemptions, total, err := model.SearchRedemptions(keyword, (p-1)*pageSize, pageSize)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
@@ -42,7 +60,12 @@ func SearchRedemptions(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": redemptions,
"data": gin.H{
"items": redemptions,
"total": total,
"page": p,
"page_size": pageSize,
},
})
return
}

View File

@@ -11,9 +11,10 @@ import (
"strings"
"sync"
"one-api/constant"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"one-api/constant"
)
type LoginRequest struct {
@@ -242,10 +243,14 @@ func Register(c *gin.Context) {
func GetAllUsers(c *gin.Context) {
p, _ := strconv.Atoi(c.Query("p"))
if p < 0 {
p = 0
pageSize, _ := strconv.Atoi(c.Query("page_size"))
if p < 1 {
p = 1
}
users, err := model.GetAllUsers(p*common.ItemsPerPage, common.ItemsPerPage)
if pageSize < 0 {
pageSize = common.ItemsPerPage
}
users, total, err := model.GetAllUsers((p-1)*pageSize, pageSize)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
@@ -256,7 +261,12 @@ func GetAllUsers(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": users,
"data": gin.H{
"items": users,
"total": total,
"page": p,
"page_size": pageSize,
},
})
return
}
@@ -264,7 +274,16 @@ func GetAllUsers(c *gin.Context) {
func SearchUsers(c *gin.Context) {
keyword := c.Query("keyword")
group := c.Query("group")
users, err := model.SearchUsers(keyword, group)
p, _ := strconv.Atoi(c.Query("p"))
pageSize, _ := strconv.Atoi(c.Query("page_size"))
if p < 1 {
p = 1
}
if pageSize < 0 {
pageSize = common.ItemsPerPage
}
startIdx := (p - 1) * pageSize
users, total, err := model.SearchUsers(keyword, group, startIdx, pageSize)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
@@ -275,7 +294,12 @@ func SearchUsers(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": users,
"data": gin.H{
"items": users,
"total": total,
"page": p,
"page_size": pageSize,
},
})
return
}

View File

@@ -23,10 +23,6 @@ type Ability struct {
func GetGroupModels(group string) []string {
var models []string
// Find distinct models
groupCol := "`group`"
if common.UsingPostgreSQL {
groupCol = `"group"`
}
DB.Table("abilities").Where(groupCol+" = ? and enabled = ?", group, true).Distinct("model").Pluck("model", &models)
return models
}
@@ -45,10 +41,8 @@ func GetAllEnableAbilities() []Ability {
}
func getPriority(group string, model string, retry int) (int, error) {
groupCol := "`group`"
trueVal := "1"
if common.UsingPostgreSQL {
groupCol = `"group"`
trueVal = "true"
}
@@ -81,10 +75,8 @@ func getPriority(group string, model string, retry int) (int, error) {
}
func getChannelQuery(group string, model string, retry int) *gorm.DB {
groupCol := "`group`"
trueVal := "1"
if common.UsingPostgreSQL {
groupCol = `"group"`
trueVal = "true"
}
maxPrioritySubQuery := DB.Model(&Ability{}).Select("MAX(priority)").Where(groupCol+" = ? and model = ? and enabled = "+trueVal, group, model)

View File

@@ -114,15 +114,11 @@ func GetChannelsByTag(tag string, idSort bool) ([]*Channel, error) {
func SearchChannels(keyword string, group string, model string, idSort bool) ([]*Channel, error) {
var channels []*Channel
keyCol := "`key`"
groupCol := "`group`"
modelsCol := "`models`"
// 如果是 PostgreSQL使用双引号
if common.UsingPostgreSQL {
keyCol = `"key"`
groupCol = `"group"`
modelsCol = `"models"`
}
order := "priority desc"
@@ -437,14 +433,10 @@ func GetPaginatedTags(offset int, limit int) ([]*string, error) {
func SearchTags(keyword string, group string, model string, idSort bool) ([]*string, error) {
var tags []*string
keyCol := "`key`"
groupCol := "`group`"
modelsCol := "`models`"
// 如果是 PostgreSQL使用双引号
if common.UsingPostgreSQL {
keyCol = `"key"`
groupCol = `"group"`
modelsCol = `"models"`
}

View File

@@ -56,7 +56,7 @@ func formatUserLogs(logs []*Log) {
func GetLogByKey(key string) (logs []*Log, err error) {
if os.Getenv("LOG_SQL_DSN") != "" {
var tk Token
if err = DB.Model(&Token{}).Where("`key`=?", strings.TrimPrefix(key, "sk-")).First(&tk).Error; err != nil {
if err = DB.Model(&Token{}).Where(keyCol+"=?", strings.TrimPrefix(key, "sk-")).First(&tk).Error; err != nil {
return nil, err
}
err = LOG_DB.Model(&Log{}).Where("token_id=?", tk.Id).Find(&logs).Error

View File

@@ -16,7 +16,7 @@ import (
var groupCol string
var keyCol string
func init() {
func initCol() {
if common.UsingPostgreSQL {
groupCol = `"group"`
keyCol = `"key"`
@@ -55,6 +55,9 @@ func createRootAccountIfNeed() error {
}
func chooseDB(envName string) (*gorm.DB, error) {
defer func() {
initCol()
}()
dsn := os.Getenv(envName)
if dsn != "" {
if strings.HasPrefix(dsn, "postgres://") {

View File

@@ -3,8 +3,10 @@ package model
import (
"errors"
"fmt"
"gorm.io/gorm"
"one-api/common"
"strconv"
"gorm.io/gorm"
)
type Redemption struct {
@@ -21,16 +23,80 @@ type Redemption struct {
DeletedAt gorm.DeletedAt `gorm:"index"`
}
func GetAllRedemptions(startIdx int, num int) ([]*Redemption, error) {
var redemptions []*Redemption
var err error
err = DB.Order("id desc").Limit(num).Offset(startIdx).Find(&redemptions).Error
return redemptions, err
func GetAllRedemptions(startIdx int, num int) (redemptions []*Redemption, total int64, err error) {
// 开始事务
tx := DB.Begin()
if tx.Error != nil {
return nil, 0, tx.Error
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 获取总数
err = tx.Model(&Redemption{}).Count(&total).Error
if err != nil {
tx.Rollback()
return nil, 0, err
}
// 获取分页数据
err = tx.Order("id desc").Limit(num).Offset(startIdx).Find(&redemptions).Error
if err != nil {
tx.Rollback()
return nil, 0, err
}
// 提交事务
if err = tx.Commit().Error; err != nil {
return nil, 0, err
}
return redemptions, total, nil
}
func SearchRedemptions(keyword string) (redemptions []*Redemption, err error) {
err = DB.Where("id = ? or name LIKE ?", keyword, keyword+"%").Find(&redemptions).Error
return redemptions, err
func SearchRedemptions(keyword string, startIdx int, num int) (redemptions []*Redemption, total int64, err error) {
tx := DB.Begin()
if tx.Error != nil {
return nil, 0, tx.Error
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// Build query based on keyword type
query := tx.Model(&Redemption{})
// Only try to convert to ID if the string represents a valid integer
if id, err := strconv.Atoi(keyword); err == nil {
query = query.Where("id = ? OR name LIKE ?", id, keyword+"%")
} else {
query = query.Where("name LIKE ?", keyword+"%")
}
// Get total count
err = query.Count(&total).Error
if err != nil {
tx.Rollback()
return nil, 0, err
}
// Get paginated data
err = query.Order("id desc").Limit(num).Offset(startIdx).Find(&redemptions).Error
if err != nil {
tx.Rollback()
return nil, 0, err
}
if err = tx.Commit().Error; err != nil {
return nil, 0, err
}
return redemptions, total, nil
}
func GetRedemptionById(id int) (*Redemption, error) {

View File

@@ -68,7 +68,7 @@ func SearchUserTokens(userId int, keyword string, token string) (tokens []*Token
if token != "" {
token = strings.Trim(token, "sk-")
}
err = DB.Where("user_id = ?", userId).Where("name LIKE ?", "%"+keyword+"%").Where("`key` LIKE ?", "%"+token+"%").Find(&tokens).Error
err = DB.Where("user_id = ?", userId).Where("name LIKE ?", "%"+keyword+"%").Where(keyCol+" LIKE ?", "%"+token+"%").Find(&tokens).Error
return tokens, err
}
@@ -133,7 +133,7 @@ func GetTokenById(id int) (*Token, error) {
token := Token{Id: id}
var err error = nil
err = DB.First(&token, "id = ?", id).Error
if err != nil {
if shouldUpdateRedis(true, err) {
gopool.Go(func() {
if err := cacheSetToken(token); err != nil {
common.SysError("failed to update user status cache: " + err.Error())
@@ -176,7 +176,7 @@ func (token *Token) Insert() error {
// Update Make sure your token's fields is completed, because this will update non-zero values
func (token *Token) Update() (err error) {
defer func() {
if common.RedisEnabled && err == nil {
if shouldUpdateRedis(true, err) {
gopool.Go(func() {
err := cacheSetToken(*token)
if err != nil {
@@ -192,7 +192,7 @@ func (token *Token) Update() (err error) {
func (token *Token) SelectUpdate() (err error) {
defer func() {
if common.RedisEnabled && err == nil {
if shouldUpdateRedis(true, err) {
gopool.Go(func() {
err := cacheSetToken(*token)
if err != nil {
@@ -207,7 +207,7 @@ func (token *Token) SelectUpdate() (err error) {
func (token *Token) Delete() (err error) {
defer func() {
if common.RedisEnabled && err == nil {
if shouldUpdateRedis(true, err) {
gopool.Go(func() {
err := cacheDeleteToken(token.Key)
if err != nil {
@@ -267,12 +267,14 @@ func IncreaseTokenQuota(id int, key string, quota int) (err error) {
if quota < 0 {
return errors.New("quota 不能为负数!")
}
gopool.Go(func() {
err := cacheIncrTokenQuota(key, int64(quota))
if err != nil {
common.SysError("failed to increase token quota: " + err.Error())
}
})
if common.RedisEnabled {
gopool.Go(func() {
err := cacheIncrTokenQuota(key, int64(quota))
if err != nil {
common.SysError("failed to increase token quota: " + err.Error())
}
})
}
if common.BatchUpdateEnabled {
addNewRecord(BatchUpdateTypeTokenQuota, id, quota)
return nil
@@ -295,12 +297,14 @@ func DecreaseTokenQuota(id int, key string, quota int) (err error) {
if quota < 0 {
return errors.New("quota 不能为负数!")
}
gopool.Go(func() {
err := cacheDecrTokenQuota(key, int64(quota))
if err != nil {
common.SysError("failed to decrease token quota: " + err.Error())
}
})
if common.RedisEnabled {
gopool.Go(func() {
err := cacheDecrTokenQuota(key, int64(quota))
if err != nil {
common.SysError("failed to decrease token quota: " + err.Error())
}
})
}
if common.BatchUpdateEnabled {
addNewRecord(BatchUpdateTypeTokenQuota, id, -quota)
return nil

View File

@@ -81,41 +81,100 @@ func GetMaxUserId() int {
return user.Id
}
func GetAllUsers(startIdx int, num int) (users []*User, err error) {
err = DB.Unscoped().Order("id desc").Limit(num).Offset(startIdx).Omit("password").Find(&users).Error
return users, err
func GetAllUsers(startIdx int, num int) (users []*User, total int64, err error) {
// Start transaction
tx := DB.Begin()
if tx.Error != nil {
return nil, 0, tx.Error
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// Get total count within transaction
err = tx.Unscoped().Model(&User{}).Count(&total).Error
if err != nil {
tx.Rollback()
return nil, 0, err
}
// Get paginated users within same transaction
err = tx.Unscoped().Order("id desc").Limit(num).Offset(startIdx).Omit("password").Find(&users).Error
if err != nil {
tx.Rollback()
return nil, 0, err
}
// Commit transaction
if err = tx.Commit().Error; err != nil {
return nil, 0, err
}
return users, total, nil
}
func SearchUsers(keyword string, group string) ([]*User, error) {
func SearchUsers(keyword string, group string, startIdx int, num int) ([]*User, int64, error) {
var users []*User
var total int64
var err error
// 开始事务
tx := DB.Begin()
if tx.Error != nil {
return nil, 0, tx.Error
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 构建基础查询
query := tx.Unscoped().Model(&User{})
// 尝试将关键字转换为整数ID
keywordInt, err := strconv.Atoi(keyword)
if err == nil {
// 如果转换成功按照ID和可选的组别搜索用户
query := DB.Unscoped().Omit("password").Where("id = ?", keywordInt)
if group != "" {
query = query.Where(groupCol+" = ?", group) // 使用反引号包围group
query = query.Where("id = ? AND "+groupCol+" = ?", keywordInt, group)
} else {
query = query.Where("id = ?", keywordInt)
}
err = query.Find(&users).Error
if err != nil || len(users) > 0 {
return users, err
}
}
err = nil
query := DB.Unscoped().Omit("password")
likeCondition := "username LIKE ? OR email LIKE ? OR display_name LIKE ?"
if group != "" {
query = query.Where("("+likeCondition+") AND "+groupCol+" = ?", "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%", group)
} else {
query = query.Where(likeCondition, "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%")
// 如果不是ID搜索则使用模糊匹配
likeCondition := "username LIKE ? OR email LIKE ? OR display_name LIKE ?"
if group != "" {
query = query.Where("("+likeCondition+") AND "+groupCol+" = ?",
"%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%", group)
} else {
query = query.Where(likeCondition,
"%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%")
}
}
err = query.Find(&users).Error
return users, err
// 获取总数
err = query.Count(&total).Error
if err != nil {
tx.Rollback()
return nil, 0, err
}
// 获取分页数据
err = query.Omit("password").Order("id desc").Limit(num).Offset(startIdx).Find(&users).Error
if err != nil {
tx.Rollback()
return nil, 0, err
}
// 提交事务
if err = tx.Commit().Error; err != nil {
return nil, 0, err
}
return users, total, nil
}
func GetUserById(id int, selectAll bool) (*User, error) {

View File

@@ -825,6 +825,12 @@ const LogsTable = () => {
dataSource={logs}
rowKey="key"
pagination={{
formatPageText: (page) =>
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart,
end: page.currentEnd,
total: logs.length
}),
currentPage: activePage,
pageSize: pageSize,
total: logCount,

View File

@@ -178,6 +178,7 @@ const RedemptionsTable = () => {
const [searching, setSearching] = useState(false);
const [tokenCount, setTokenCount] = useState(ITEMS_PER_PAGE);
const [selectedKeys, setSelectedKeys] = useState([]);
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
const [editingRedemption, setEditingRedemption] = useState({
id: undefined,
});
@@ -187,40 +188,20 @@ const RedemptionsTable = () => {
setShowEdit(false);
};
// const setCount = (data) => {
// if (data.length >= (activePage) * ITEMS_PER_PAGE) {
// setTokenCount(data.length + 1);
// } else {
// setTokenCount(data.length);
// }
// }
const setRedemptionFormat = (redeptions) => {
// for (let i = 0; i < redeptions.length; i++) {
// redeptions[i].key = '' + redeptions[i].id;
// }
// data.key = '' + data.id
setRedemptions(redeptions);
if (redeptions.length >= activePage * ITEMS_PER_PAGE) {
setTokenCount(redeptions.length + 1);
} else {
setTokenCount(redeptions.length);
}
};
const loadRedemptions = async (startIdx) => {
const res = await API.get(`/api/redemption/?p=${startIdx}`);
const loadRedemptions = async (startIdx, pageSize) => {
const res = await API.get(`/api/redemption/?p=${startIdx}&page_size=${pageSize}`);
const { success, message, data } = res.data;
if (success) {
if (startIdx === 0) {
setRedemptionFormat(data);
} else {
let newRedemptions = redemptions;
newRedemptions.push(...data);
setRedemptionFormat(newRedemptions);
}
const newPageData = data.items;
setActivePage(data.page);
setTokenCount(data.total);
setRedemptionFormat(newPageData);
} else {
showError(message);
showError(message);
}
setLoading(false);
};
@@ -248,16 +229,15 @@ const RedemptionsTable = () => {
const onPaginationChange = (e, { activePage }) => {
(async () => {
if (activePage === Math.ceil(redemptions.length / ITEMS_PER_PAGE) + 1) {
// In this case we have to load more data and then append them.
await loadRedemptions(activePage - 1);
if (activePage === Math.ceil(redemptions.length / pageSize) + 1) {
await loadRedemptions(activePage - 1, pageSize);
}
setActivePage(activePage);
})();
};
useEffect(() => {
loadRedemptions(0)
loadRedemptions(0, pageSize)
.then()
.catch((reason) => {
showError(reason);
@@ -265,7 +245,7 @@ const RedemptionsTable = () => {
}, []);
const refresh = async () => {
await loadRedemptions(activePage - 1);
await loadRedemptions(activePage - 1, pageSize);
};
const manageRedemption = async (id, action, record) => {
@@ -300,23 +280,21 @@ const RedemptionsTable = () => {
}
};
const searchRedemptions = async () => {
const searchRedemptions = async (keyword, page, pageSize) => {
if (searchKeyword === '') {
// if keyword is blank, load files instead.
await loadRedemptions(0);
setActivePage(1);
return;
await loadRedemptions(page, pageSize);
return;
}
setSearching(true);
const res = await API.get(
`/api/redemption/search?keyword=${searchKeyword}`,
);
const res = await API.get(`/api/redemption/search?keyword=${keyword}&p=${page}&page_size=${pageSize}`);
const { success, message, data } = res.data;
if (success) {
setRedemptions(data);
setActivePage(1);
const newPageData = data.items;
setActivePage(data.page);
setTokenCount(data.total);
setRedemptionFormat(newPageData);
} else {
showError(message);
showError(message);
}
setSearching(false);
};
@@ -341,16 +319,14 @@ const RedemptionsTable = () => {
const handlePageChange = (page) => {
setActivePage(page);
if (page === Math.ceil(redemptions.length / ITEMS_PER_PAGE) + 1) {
// In this case we have to load more data and then append them.
loadRedemptions(page - 1).then((r) => {});
if (searchKeyword === '') {
loadRedemptions(page, pageSize).then();
} else {
searchRedemptions(searchKeyword, page, pageSize).then();
}
};
let pageData = redemptions.slice(
(activePage - 1) * ITEMS_PER_PAGE,
activePage * ITEMS_PER_PAGE,
);
let pageData = redemptions;
const rowSelection = {
onSelect: (record, selected) => {},
onSelectAll: (selected, selectedRows) => {},
@@ -379,7 +355,9 @@ const RedemptionsTable = () => {
visiable={showEdit}
handleClose={closeEdit}
></EditRedemption>
<Form onSubmit={searchRedemptions}>
<Form onSubmit={()=> {
searchRedemptions(searchKeyword, activePage, pageSize).then();
}}>
<Form.Input
label={t('搜索关键字')}
field='keyword'
@@ -431,20 +409,25 @@ const RedemptionsTable = () => {
dataSource={pageData}
pagination={{
currentPage: activePage,
pageSize: ITEMS_PER_PAGE,
pageSize: pageSize,
total: tokenCount,
// showSizeChanger: true,
// pageSizeOptions: [10, 20, 50, 100],
showSizeChanger: true,
pageSizeOpts: [10, 20, 50, 100],
formatPageText: (page) =>
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart,
end: page.currentEnd,
total: redemptions.length
total: tokenCount
}),
// onPageSizeChange: (size) => {
// setPageSize(size);
// setActivePage(1);
// },
onPageSizeChange: (size) => {
setPageSize(size);
setActivePage(1);
if (searchKeyword === '') {
loadRedemptions(1, size).then();
} else {
searchRedemptions(searchKeyword, 1, size).then();
}
},
onPageChange: handlePageChange,
}}
loading={loading}

View File

@@ -231,6 +231,7 @@ const UsersTable = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [activePage, setActivePage] = useState(1);
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
const [searchKeyword, setSearchKeyword] = useState('');
const [searching, setSearching] = useState(false);
const [searchGroup, setSearchGroup] = useState('');
@@ -242,14 +243,6 @@ const UsersTable = () => {
id: undefined,
});
const setCount = (data) => {
if (data.length >= activePage * ITEMS_PER_PAGE) {
setUserCount(data.length + 1);
} else {
setUserCount(data.length);
}
};
const removeRecord = (key) => {
let newDataSource = [...users];
if (key != null) {
@@ -263,37 +256,30 @@ const UsersTable = () => {
}
};
const loadUsers = async (startIdx) => {
const res = await API.get(`/api/user/?p=${startIdx}`);
const setUserFormat = (users) => {
for (let i = 0; i < users.length; i++) {
users[i].key = users[i].id;
}
setUsers(users);
}
const loadUsers = async (startIdx, pageSize) => {
const res = await API.get(`/api/user/?p=${startIdx}&page_size=${pageSize}`);
const { success, message, data } = res.data;
if (success) {
if (startIdx === 0) {
setUsers(data);
setCount(data);
} else {
let newUsers = users;
newUsers.push(...data);
setUsers(newUsers);
setCount(newUsers);
}
const newPageData = data.items;
setActivePage(data.page);
setUserCount(data.total);
setUserFormat(newPageData);
} else {
showError(message);
}
setLoading(false);
};
const onPaginationChange = (e, { activePage }) => {
(async () => {
if (activePage === Math.ceil(users.length / ITEMS_PER_PAGE) + 1) {
// In this case we have to load more data and then append them.
await loadUsers(activePage - 1);
}
setActivePage(activePage);
})();
};
useEffect(() => {
loadUsers(0)
loadUsers(0, pageSize)
.then()
.catch((reason) => {
showError(reason);
@@ -341,21 +327,22 @@ const UsersTable = () => {
}
};
const searchUsers = async (searchKeyword, searchGroup) => {
const searchUsers = async (startIdx, pageSize, searchKeyword, searchGroup) => {
if (searchKeyword === '' && searchGroup === '') {
// if keyword is blank, load files instead.
await loadUsers(0);
setActivePage(1);
return;
// if keyword is blank, load files instead.
await loadUsers(startIdx, pageSize);
return;
}
setSearching(true);
const res = await API.get(`/api/user/search?keyword=${searchKeyword}&group=${searchGroup}`);
const res = await API.get(`/api/user/search?keyword=${searchKeyword}&group=${searchGroup}&p=${startIdx}&page_size=${pageSize}`);
const { success, message, data } = res.data;
if (success) {
setUsers(data);
setActivePage(1);
const newPageData = data.items;
setActivePage(data.page);
setUserCount(data.total);
setUserFormat(newPageData);
} else {
showError(message);
showError(message);
}
setSearching(false);
};
@@ -364,33 +351,15 @@ const UsersTable = () => {
setSearchKeyword(value.trim());
};
const sortUser = (key) => {
if (users.length === 0) return;
setLoading(true);
let sortedUsers = [...users];
sortedUsers.sort((a, b) => {
return ('' + a[key]).localeCompare(b[key]);
});
if (sortedUsers[0].id === users[0].id) {
sortedUsers.reverse();
}
setUsers(sortedUsers);
setLoading(false);
};
const handlePageChange = (page) => {
setActivePage(page);
if (page === Math.ceil(users.length / ITEMS_PER_PAGE) + 1) {
// In this case we have to load more data and then append them.
loadUsers(page - 1).then((r) => {});
if (searchKeyword === '' && searchGroup === '') {
loadUsers(page, pageSize).then();
} else {
searchUsers(page, pageSize, searchKeyword, searchGroup).then();
}
};
const pageData = users.slice(
(activePage - 1) * ITEMS_PER_PAGE,
activePage * ITEMS_PER_PAGE,
);
const closeAddUser = () => {
setShowAddUser(false);
};
@@ -403,8 +372,9 @@ const UsersTable = () => {
};
const refresh = async () => {
setActivePage(1)
if (searchKeyword === '') {
await loadUsers(activePage - 1);
await loadUsers(activePage, pageSize);
} else {
await searchUsers(searchKeyword, searchGroup);
}
@@ -429,6 +399,17 @@ const UsersTable = () => {
}
};
const handlePageSizeChange = async (size) => {
localStorage.setItem('page-size', size + '');
setPageSize(size);
setActivePage(1);
loadUsers(activePage, size)
.then()
.catch((reason) => {
showError(reason);
});
};
return (
<>
<AddUser
@@ -444,29 +425,32 @@ const UsersTable = () => {
></EditUser>
<Form
onSubmit={() => {
searchUsers(searchKeyword, searchGroup);
searchUsers(activePage, pageSize, searchKeyword, searchGroup);
}}
labelPosition='left'
>
<div style={{ display: 'flex' }}>
<Space>
<Form.Input
label={t('搜索关键字')}
icon='search'
field='keyword'
iconPosition='left'
placeholder={t('搜索用户的 ID用户名显示名称以及邮箱地址 ...')}
value={searchKeyword}
loading={searching}
onChange={(value) => handleKeywordChange(value)}
/>
<Tooltip content={t('支持搜索用户的 ID、用户名、显示名称和邮箱地址')}>
<Form.Input
label={t('搜索关键字')}
icon='search'
field='keyword'
iconPosition='left'
placeholder={t('搜索关键字')}
value={searchKeyword}
loading={searching}
onChange={(value) => handleKeywordChange(value)}
/>
</Tooltip>
<Form.Select
field='group'
label={t('分组')}
optionList={groupOptions}
onChange={(value) => {
setSearchGroup(value);
searchUsers(searchKeyword, value);
searchUsers(activePage, pageSize, searchKeyword, value);
}}
/>
<Button
@@ -492,7 +476,7 @@ const UsersTable = () => {
<Table
columns={columns}
dataSource={pageData}
dataSource={users}
pagination={{
formatPageText: (page) =>
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
@@ -501,9 +485,13 @@ const UsersTable = () => {
total: users.length
}),
currentPage: activePage,
pageSize: ITEMS_PER_PAGE,
pageSize: pageSize,
total: userCount,
pageSizeOpts: [10, 20, 50, 100],
showSizeChanger: true,
onPageSizeChange: (size) => {
handlePageSizeChange(size);
},
onPageChange: handlePageChange,
}}
loading={loading}

View File

@@ -1,5 +1,5 @@
import i18next from 'i18next';
import { Modal, Tag } from '@douyinfe/semi-ui';
import { Modal, Tag, Typography } from '@douyinfe/semi-ui';
import { copy, showSuccess } from './utils.js';
export function renderText(text, limit) {
@@ -55,6 +55,81 @@ export function renderGroup(group) {
);
}
export function renderRatio(ratio) {
let color = 'green';
if (ratio > 5) {
color = 'red';
} else if (ratio > 3) {
color = 'orange';
} else if (ratio > 1) {
color = 'blue';
}
return <Tag color={color}>{ratio}x {i18next.t('倍率')}</Tag>;
}
export const renderGroupOption = (item) => {
const {
disabled,
selected,
label,
value,
focused,
className,
style,
onMouseEnter,
onClick,
empty,
emptyContent,
...rest
} = item;
const baseStyle = {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '8px 16px',
cursor: disabled ? 'not-allowed' : 'pointer',
backgroundColor: focused ? 'var(--semi-color-fill-0)' : 'transparent',
opacity: disabled ? 0.5 : 1,
...(selected && {
backgroundColor: 'var(--semi-color-primary-light-default)',
}),
'&:hover': {
backgroundColor: !disabled && 'var(--semi-color-fill-1)'
}
};
const handleClick = () => {
if (!disabled && onClick) {
onClick();
}
};
const handleMouseEnter = (e) => {
if (!disabled && onMouseEnter) {
onMouseEnter(e);
}
};
return (
<div
style={baseStyle}
onClick={handleClick}
onMouseEnter={handleMouseEnter}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<Typography.Text strong type={disabled ? 'tertiary' : undefined}>
{value}
</Typography.Text>
<Typography.Text type="secondary" size="small">
{label}
</Typography.Text>
</div>
{item.ratio && renderRatio(item.ratio)}
</div>
);
};
export function renderNumber(num) {
if (num >= 1000000000) {
return (num / 1000000000).toFixed(1) + 'B';
@@ -352,7 +427,7 @@ export const modelColorMap = {
'gpt-3.5-turbo-0613': 'rgb(60,179,113)', // 海洋绿
'gpt-3.5-turbo-1106': 'rgb(32,178,170)', // 浅海洋绿
'gpt-3.5-turbo-16k': 'rgb(149,252,206)', // 淡橙色
'gpt-3.5-turbo-16k-0613': 'rgb(119,255,214)', // 淡桃<EFBFBD><EFBFBD><EFBFBD>
'gpt-3.5-turbo-16k-0613': 'rgb(119,255,214)', // 淡桃
'gpt-3.5-turbo-instruct': 'rgb(175,238,238)', // 粉蓝色
'gpt-4': 'rgb(135,206,235)', // 天蓝色
// 'gpt-4-0314': 'rgb(70,130,180)', // 钢蓝色
@@ -375,7 +450,7 @@ export const modelColorMap = {
'text-embedding-ada-002': 'rgb(255,182,193)', // 浅粉红
'text-embedding-v1': 'rgb(255,174,185)', // 浅粉红色(略有区别)
'text-moderation-latest': 'rgb(255,130,171)', // 强粉色
'text-moderation-stable': 'rgb(255,160,122)', // 浅珊瑚色(<EFBFBD><EFBFBD><EFBFBD>Babbage相同表示同一类功能
'text-moderation-stable': 'rgb(255,160,122)', // 浅珊瑚色(Babbage相同表示同一类功能
'tts-1': 'rgb(255,140,0)', // 深橙色
'tts-1-1106': 'rgb(255,165,0)', // 橙色
'tts-1-hd': 'rgb(255,215,0)', // 金色

View File

@@ -1240,5 +1240,7 @@
"时间范围": "Time range",
"批量设置标签": "Batch set tag",
"请输入要设置的标签名称": "Please enter the tag name to be set",
"请输入标签名称": "Please enter the tag name"
"请输入标签名称": "Please enter the tag name",
"支持搜索用户的 ID、用户名、显示名称和邮箱地址": "Support searching for user ID, username, display name, and email address",
"已注销": "Logged out"
}

View File

@@ -2,11 +2,12 @@ import React, { useCallback, useContext, useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { UserContext } from '../../context/User/index.js';
import { API, getUserIdFromLocalStorage, showError } from '../../helpers/index.js';
import { Card, Chat, Input, Layout, Select, Slider, TextArea, Typography, Button } from '@douyinfe/semi-ui';
import { Card, Chat, Input, Layout, Select, Slider, TextArea, Typography, Button, Highlight } from '@douyinfe/semi-ui';
import { SSE } from 'sse';
import { IconSetting } from '@douyinfe/semi-icons';
import { StyleContext } from '../../context/Style/index.js';
import { useTranslation } from 'react-i18next';
import { renderGroupOption } from '../../helpers/render.js';
const roleInfo = {
user: {
@@ -97,15 +98,17 @@ const Playground = () => {
let res = await API.get(`/api/user/self/groups`);
const { success, message, data } = res.data;
if (success) {
let localGroupOptions = Object.keys(data).map((group) => ({
label: data[group],
let localGroupOptions = Object.entries(data).map(([group, info]) => ({
label: info.desc,
value: group,
ratio: info.ratio
}));
if (localGroupOptions.length === 0) {
localGroupOptions = [{
label: t('用户分组'),
value: '',
ratio: 1
}];
} else {
const localUser = JSON.parse(localStorage.getItem('user'));
@@ -326,12 +329,9 @@ const Playground = () => {
}}
value={inputs.group}
autoComplete='new-password'
optionList={groups.map((group) => ({
...group,
label: styleState.isMobile && group.label.length > 16
? group.label.substring(0, 16) + '...'
: group.label,
}))}
optionList={groups}
renderOptionItem={renderGroupOption}
style={{ width: '100%' }}
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>{t('模型')}</Typography.Text>

View File

@@ -7,7 +7,7 @@ import {
showSuccess,
timestamp2string,
} from '../../helpers';
import { renderQuotaWithPrompt } from '../../helpers/render';
import { renderGroupOption, renderQuotaWithPrompt } from '../../helpers/render';
import {
AutoComplete,
Banner,
@@ -97,9 +97,10 @@ const EditToken = (props) => {
let res = await API.get(`/api/user/self/groups`);
const { success, message, data } = res.data;
if (success) {
let localGroupOptions = Object.keys(data).map((group) => ({
label: data[group],
let localGroupOptions = Object.entries(data).map(([group, info]) => ({
label: info.desc,
value: group,
ratio: info.ratio
}));
setGroups(localGroupOptions);
} else {
@@ -449,6 +450,8 @@ const EditToken = (props) => {
onChange={(value) => {
handleInputChange('group', value);
}}
position={'topLeft'}
renderOptionItem={renderGroupOption}
value={inputs.group}
autoComplete='new-password'
optionList={groups}

View File

@@ -19,7 +19,11 @@ const AddUser = (props) => {
const submit = async () => {
setLoading(true);
if (inputs.username === '' || inputs.password === '') return;
if (inputs.username === '' || inputs.password === '') {
setLoading(false);
showError('用户名和密码不能为空!');
return;
}
const res = await API.post(`/api/user/`, inputs);
const { success, message } = res.data;
if (success) {