Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
1ecd8b41
authored
Aug 04, 2025
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: enhance multi-key management with pagination and statistics
parent
aeec39a1
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
227 additions
and
52 deletions
+227
-52
controller/channel.go
+99
-21
model/channel.go
+6
-6
web/src/components/table/channels/modals/MultiKeyManageModal.jsx
+122
-25
No files found.
controller/channel.go
View file @
1ecd8b41
...
@@ -71,6 +71,13 @@ func parseStatusFilter(statusParam string) int {
...
@@ -71,6 +71,13 @@ func parseStatusFilter(statusParam string) int {
}
}
}
}
func
clearChannelInfo
(
channel
*
model
.
Channel
)
{
if
channel
.
ChannelInfo
.
IsMultiKey
{
channel
.
ChannelInfo
.
MultiKeyDisabledReason
=
nil
channel
.
ChannelInfo
.
MultiKeyDisabledTime
=
nil
}
}
func
GetAllChannels
(
c
*
gin
.
Context
)
{
func
GetAllChannels
(
c
*
gin
.
Context
)
{
pageInfo
:=
common
.
GetPageQuery
(
c
)
pageInfo
:=
common
.
GetPageQuery
(
c
)
channelData
:=
make
([]
*
model
.
Channel
,
0
)
channelData
:=
make
([]
*
model
.
Channel
,
0
)
...
@@ -145,6 +152,10 @@ func GetAllChannels(c *gin.Context) {
...
@@ -145,6 +152,10 @@ func GetAllChannels(c *gin.Context) {
}
}
}
}
for
_
,
datum
:=
range
channelData
{
clearChannelInfo
(
datum
)
}
countQuery
:=
model
.
DB
.
Model
(
&
model
.
Channel
{})
countQuery
:=
model
.
DB
.
Model
(
&
model
.
Channel
{})
if
statusFilter
==
common
.
ChannelStatusEnabled
{
if
statusFilter
==
common
.
ChannelStatusEnabled
{
countQuery
=
countQuery
.
Where
(
"status = ?"
,
common
.
ChannelStatusEnabled
)
countQuery
=
countQuery
.
Where
(
"status = ?"
,
common
.
ChannelStatusEnabled
)
...
@@ -371,6 +382,10 @@ func SearchChannels(c *gin.Context) {
...
@@ -371,6 +382,10 @@ func SearchChannels(c *gin.Context) {
pagedData
:=
channelData
[
startIdx
:
endIdx
]
pagedData
:=
channelData
[
startIdx
:
endIdx
]
for
_
,
datum
:=
range
pagedData
{
clearChannelInfo
(
datum
)
}
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"success"
:
true
,
"message"
:
""
,
"message"
:
""
,
...
@@ -394,6 +409,9 @@ func GetChannel(c *gin.Context) {
...
@@ -394,6 +409,9 @@ func GetChannel(c *gin.Context) {
common
.
ApiError
(
c
,
err
)
common
.
ApiError
(
c
,
err
)
return
return
}
}
if
channel
!=
nil
{
clearChannelInfo
(
channel
)
}
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"success"
:
true
,
"message"
:
""
,
"message"
:
""
,
...
@@ -827,6 +845,7 @@ func UpdateChannel(c *gin.Context) {
...
@@ -827,6 +845,7 @@ func UpdateChannel(c *gin.Context) {
}
}
model
.
InitChannelCache
()
model
.
InitChannelCache
()
channel
.
Key
=
""
channel
.
Key
=
""
clearChannelInfo
(
&
channel
.
Channel
)
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"success"
:
true
,
"message"
:
""
,
"message"
:
""
,
...
@@ -1036,11 +1055,21 @@ type MultiKeyManageRequest struct {
...
@@ -1036,11 +1055,21 @@ type MultiKeyManageRequest struct {
ChannelId
int
`json:"channel_id"`
ChannelId
int
`json:"channel_id"`
Action
string
`json:"action"`
// "disable_key", "enable_key", "delete_disabled_keys", "get_key_status"
Action
string
`json:"action"`
// "disable_key", "enable_key", "delete_disabled_keys", "get_key_status"
KeyIndex
*
int
`json:"key_index,omitempty"`
// for disable_key and enable_key actions
KeyIndex
*
int
`json:"key_index,omitempty"`
// for disable_key and enable_key actions
Page
int
`json:"page,omitempty"`
// for get_key_status pagination
PageSize
int
`json:"page_size,omitempty"`
// for get_key_status pagination
}
}
// MultiKeyStatusResponse represents the response for key status query
// MultiKeyStatusResponse represents the response for key status query
type
MultiKeyStatusResponse
struct
{
type
MultiKeyStatusResponse
struct
{
Keys
[]
KeyStatus
`json:"keys"`
Keys
[]
KeyStatus
`json:"keys"`
Total
int
`json:"total"`
Page
int
`json:"page"`
PageSize
int
`json:"page_size"`
TotalPages
int
`json:"total_pages"`
// Statistics
EnabledCount
int
`json:"enabled_count"`
ManualDisabledCount
int
`json:"manual_disabled_count"`
AutoDisabledCount
int
`json:"auto_disabled_count"`
}
}
type
KeyStatus
struct
{
type
KeyStatus
struct
{
...
@@ -1080,8 +1109,35 @@ func ManageMultiKeys(c *gin.Context) {
...
@@ -1080,8 +1109,35 @@ func ManageMultiKeys(c *gin.Context) {
switch
request
.
Action
{
switch
request
.
Action
{
case
"get_key_status"
:
case
"get_key_status"
:
keys
:=
channel
.
GetKeys
()
keys
:=
channel
.
GetKeys
()
var
keyStatusList
[]
KeyStatus
total
:=
len
(
keys
)
// Default pagination parameters
page
:=
request
.
Page
pageSize
:=
request
.
PageSize
if
page
<=
0
{
page
=
1
}
if
pageSize
<=
0
{
pageSize
=
50
// Default page size
}
// Calculate pagination
totalPages
:=
(
total
+
pageSize
-
1
)
/
pageSize
if
page
>
totalPages
&&
totalPages
>
0
{
page
=
totalPages
}
// Calculate range
start
:=
(
page
-
1
)
*
pageSize
end
:=
start
+
pageSize
if
end
>
total
{
end
=
total
}
// Statistics for all keys
var
enabledCount
,
manualDisabledCount
,
autoDisabledCount
int
var
keyStatusList
[]
KeyStatus
for
i
,
key
:=
range
keys
{
for
i
,
key
:=
range
keys
{
status
:=
1
// default enabled
status
:=
1
// default enabled
var
disabledTime
int64
var
disabledTime
int64
...
@@ -1093,34 +1149,56 @@ func ManageMultiKeys(c *gin.Context) {
...
@@ -1093,34 +1149,56 @@ func ManageMultiKeys(c *gin.Context) {
}
}
}
}
if
status
!=
1
{
// Count for statistics
if
channel
.
ChannelInfo
.
MultiKeyDisabledTime
!=
nil
{
switch
status
{
disabledTime
=
channel
.
ChannelInfo
.
MultiKeyDisabledTime
[
i
]
case
1
:
enabledCount
++
case
2
:
manualDisabledCount
++
case
3
:
autoDisabledCount
++
}
// Only include keys in current page
if
i
>=
start
&&
i
<
end
{
if
status
!=
1
{
if
channel
.
ChannelInfo
.
MultiKeyDisabledTime
!=
nil
{
disabledTime
=
channel
.
ChannelInfo
.
MultiKeyDisabledTime
[
i
]
}
if
channel
.
ChannelInfo
.
MultiKeyDisabledReason
!=
nil
{
reason
=
channel
.
ChannelInfo
.
MultiKeyDisabledReason
[
i
]
}
}
}
if
channel
.
ChannelInfo
.
MultiKeyDisabledReason
!=
nil
{
reason
=
channel
.
ChannelInfo
.
MultiKeyDisabledReason
[
i
]
// Create key preview (first 10 chars)
keyPreview
:=
key
if
len
(
key
)
>
10
{
keyPreview
=
key
[
:
10
]
+
"..."
}
}
}
// Create key preview (first 10 chars)
keyStatusList
=
append
(
keyStatusList
,
KeyStatus
{
keyPreview
:=
key
Index
:
i
,
if
len
(
key
)
>
10
{
Status
:
status
,
keyPreview
=
key
[
:
10
]
+
"..."
DisabledTime
:
disabledTime
,
Reason
:
reason
,
KeyPreview
:
keyPreview
,
})
}
}
keyStatusList
=
append
(
keyStatusList
,
KeyStatus
{
Index
:
i
,
Status
:
status
,
DisabledTime
:
disabledTime
,
Reason
:
reason
,
KeyPreview
:
keyPreview
,
})
}
}
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"success"
:
true
,
"message"
:
""
,
"message"
:
""
,
"data"
:
MultiKeyStatusResponse
{
Keys
:
keyStatusList
},
"data"
:
MultiKeyStatusResponse
{
Keys
:
keyStatusList
,
Total
:
total
,
Page
:
page
,
PageSize
:
pageSize
,
TotalPages
:
totalPages
,
EnabledCount
:
enabledCount
,
ManualDisabledCount
:
manualDisabledCount
,
AutoDisabledCount
:
autoDisabledCount
,
},
})
})
return
return
...
...
model/channel.go
View file @
1ecd8b41
...
@@ -53,12 +53,12 @@ type Channel struct {
...
@@ -53,12 +53,12 @@ type Channel struct {
}
}
type
ChannelInfo
struct
{
type
ChannelInfo
struct
{
IsMultiKey
bool
`json:"is_multi_key"`
// 是否多Key模式
IsMultiKey
bool
`json:"is_multi_key"`
// 是否多Key模式
MultiKeySize
int
`json:"multi_key_size"`
// 多Key模式下的Key数量
MultiKeySize
int
`json:"multi_key_size"`
// 多Key模式下的Key数量
MultiKeyStatusList
map
[
int
]
int
`json:"multi_key_status_list"`
// key状态列表,key index -> status
MultiKeyStatusList
map
[
int
]
int
`json:"multi_key_status_list"`
// key状态列表,key index -> status
MultiKeyDisabledReason
map
[
int
]
string
`json:"multi_key_disabled_reason"`
// key禁用原因列表,key index -> reason
MultiKeyDisabledReason
map
[
int
]
string
`json:"multi_key_disabled_reason
,omitempty
"`
// key禁用原因列表,key index -> reason
MultiKeyDisabledTime
map
[
int
]
int64
`json:"multi_key_disabled_time"`
// key禁用时间列表,key index -> time
MultiKeyDisabledTime
map
[
int
]
int64
`json:"multi_key_disabled_time
,omitempty
"`
// key禁用时间列表,key index -> time
MultiKeyPollingIndex
int
`json:"multi_key_polling_index"`
// 多Key模式下轮询的key索引
MultiKeyPollingIndex
int
`json:"multi_key_polling_index"`
// 多Key模式下轮询的key索引
MultiKeyMode
constant
.
MultiKeyMode
`json:"multi_key_mode"`
MultiKeyMode
constant
.
MultiKeyMode
`json:"multi_key_mode"`
}
}
...
...
web/src/components/table/channels/modals/MultiKeyManageModal.jsx
View file @
1ecd8b41
...
@@ -30,7 +30,9 @@ import {
...
@@ -30,7 +30,9 @@ import {
Popconfirm
,
Popconfirm
,
Empty
,
Empty
,
Spin
,
Spin
,
Banner
Banner
,
Select
,
Pagination
}
from
'@douyinfe/semi-ui'
;
}
from
'@douyinfe/semi-ui'
;
import
{
import
{
IconRefresh
,
IconRefresh
,
...
@@ -53,24 +55,48 @@ const MultiKeyManageModal = ({
...
@@ -53,24 +55,48 @@ const MultiKeyManageModal = ({
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
keyStatusList
,
setKeyStatusList
]
=
useState
([]);
const
[
keyStatusList
,
setKeyStatusList
]
=
useState
([]);
const
[
operationLoading
,
setOperationLoading
]
=
useState
({});
const
[
operationLoading
,
setOperationLoading
]
=
useState
({});
// Pagination states
const
[
currentPage
,
setCurrentPage
]
=
useState
(
1
);
const
[
pageSize
,
setPageSize
]
=
useState
(
50
);
const
[
total
,
setTotal
]
=
useState
(
0
);
const
[
totalPages
,
setTotalPages
]
=
useState
(
0
);
// Statistics states
const
[
enabledCount
,
setEnabledCount
]
=
useState
(
0
);
const
[
manualDisabledCount
,
setManualDisabledCount
]
=
useState
(
0
);
const
[
autoDisabledCount
,
setAutoDisabledCount
]
=
useState
(
0
);
// Load key status data
// Load key status data
const
loadKeyStatus
=
async
()
=>
{
const
loadKeyStatus
=
async
(
page
=
currentPage
,
size
=
pageSize
)
=>
{
if
(
!
channel
?.
id
)
return
;
if
(
!
channel
?.
id
)
return
;
setLoading
(
true
);
setLoading
(
true
);
try
{
try
{
const
res
=
await
API
.
post
(
'/api/channel/multi_key/manage'
,
{
const
res
=
await
API
.
post
(
'/api/channel/multi_key/manage'
,
{
channel_id
:
channel
.
id
,
channel_id
:
channel
.
id
,
action
:
'get_key_status'
action
:
'get_key_status'
,
page
:
page
,
page_size
:
size
});
});
if
(
res
.
data
.
success
)
{
if
(
res
.
data
.
success
)
{
setKeyStatusList
(
res
.
data
.
data
.
keys
||
[]);
const
data
=
res
.
data
.
data
;
setKeyStatusList
(
data
.
keys
||
[]);
setTotal
(
data
.
total
||
0
);
setCurrentPage
(
data
.
page
||
1
);
setPageSize
(
data
.
page_size
||
50
);
setTotalPages
(
data
.
total_pages
||
0
);
// Update statistics
setEnabledCount
(
data
.
enabled_count
||
0
);
setManualDisabledCount
(
data
.
manual_disabled_count
||
0
);
setAutoDisabledCount
(
data
.
auto_disabled_count
||
0
);
}
else
{
}
else
{
showError
(
res
.
data
.
message
);
showError
(
res
.
data
.
message
);
}
}
}
catch
(
error
)
{
}
catch
(
error
)
{
console
.
error
(
error
);
showError
(
t
(
'获取密钥状态失败'
));
showError
(
t
(
'获取密钥状态失败'
));
}
finally
{
}
finally
{
setLoading
(
false
);
setLoading
(
false
);
...
@@ -91,7 +117,7 @@ const MultiKeyManageModal = ({
...
@@ -91,7 +117,7 @@ const MultiKeyManageModal = ({
if
(
res
.
data
.
success
)
{
if
(
res
.
data
.
success
)
{
showSuccess
(
t
(
'密钥已禁用'
));
showSuccess
(
t
(
'密钥已禁用'
));
await
loadKeyStatus
(
);
// Reload data
await
loadKeyStatus
(
currentPage
,
pageSize
);
// Reload current page
onRefresh
&&
onRefresh
();
// Refresh parent component
onRefresh
&&
onRefresh
();
// Refresh parent component
}
else
{
}
else
{
showError
(
res
.
data
.
message
);
showError
(
res
.
data
.
message
);
...
@@ -117,7 +143,7 @@ const MultiKeyManageModal = ({
...
@@ -117,7 +143,7 @@ const MultiKeyManageModal = ({
if
(
res
.
data
.
success
)
{
if
(
res
.
data
.
success
)
{
showSuccess
(
t
(
'密钥已启用'
));
showSuccess
(
t
(
'密钥已启用'
));
await
loadKeyStatus
(
);
// Reload data
await
loadKeyStatus
(
currentPage
,
pageSize
);
// Reload current page
onRefresh
&&
onRefresh
();
// Refresh parent component
onRefresh
&&
onRefresh
();
// Refresh parent component
}
else
{
}
else
{
showError
(
res
.
data
.
message
);
showError
(
res
.
data
.
message
);
...
@@ -141,7 +167,9 @@ const MultiKeyManageModal = ({
...
@@ -141,7 +167,9 @@ const MultiKeyManageModal = ({
if
(
res
.
data
.
success
)
{
if
(
res
.
data
.
success
)
{
showSuccess
(
res
.
data
.
message
);
showSuccess
(
res
.
data
.
message
);
await
loadKeyStatus
();
// Reload data
// Reset to first page after deletion as data structure might change
setCurrentPage
(
1
);
await
loadKeyStatus
(
1
,
pageSize
);
onRefresh
&&
onRefresh
();
// Refresh parent component
onRefresh
&&
onRefresh
();
// Refresh parent component
}
else
{
}
else
{
showError
(
res
.
data
.
message
);
showError
(
res
.
data
.
message
);
...
@@ -153,13 +181,40 @@ const MultiKeyManageModal = ({
...
@@ -153,13 +181,40 @@ const MultiKeyManageModal = ({
}
}
};
};
// Handle page change
const
handlePageChange
=
(
page
)
=>
{
setCurrentPage
(
page
);
loadKeyStatus
(
page
,
pageSize
);
};
// Handle page size change
const
handlePageSizeChange
=
(
size
)
=>
{
setPageSize
(
size
);
setCurrentPage
(
1
);
// Reset to first page
loadKeyStatus
(
1
,
size
);
};
// Effect to load data when modal opens
// Effect to load data when modal opens
useEffect
(()
=>
{
useEffect
(()
=>
{
if
(
visible
&&
channel
?.
id
)
{
if
(
visible
&&
channel
?.
id
)
{
loadKeyStatus
();
setCurrentPage
(
1
);
// Reset to first page when opening
loadKeyStatus
(
1
,
pageSize
);
}
}
},
[
visible
,
channel
?.
id
]);
},
[
visible
,
channel
?.
id
]);
// Reset pagination when modal closes
useEffect
(()
=>
{
if
(
!
visible
)
{
setCurrentPage
(
1
);
setKeyStatusList
([]);
setTotal
(
0
);
setTotalPages
(
0
);
setEnabledCount
(
0
);
setManualDisabledCount
(
0
);
setAutoDisabledCount
(
0
);
}
},
[
visible
]);
// Get status tag component
// Get status tag component
const
renderStatusTag
=
(
status
)
=>
{
const
renderStatusTag
=
(
status
)
=>
{
switch
(
status
)
{
switch
(
status
)
{
...
@@ -270,12 +325,6 @@ const MultiKeyManageModal = ({
...
@@ -270,12 +325,6 @@ const MultiKeyManageModal = ({
},
},
];
];
// Calculate statistics
const
enabledCount
=
keyStatusList
.
filter
(
key
=>
key
.
status
===
1
).
length
;
const
manualDisabledCount
=
keyStatusList
.
filter
(
key
=>
key
.
status
===
2
).
length
;
const
autoDisabledCount
=
keyStatusList
.
filter
(
key
=>
key
.
status
===
3
).
length
;
const
totalCount
=
keyStatusList
.
length
;
return
(
return
(
<
Modal
<
Modal
title=
{
title=
{
...
@@ -293,7 +342,7 @@ const MultiKeyManageModal = ({
...
@@ -293,7 +342,7 @@ const MultiKeyManageModal = ({
<
Button
onClick=
{
onCancel
}
>
{
t
(
'关闭'
)
}
</
Button
>
<
Button
onClick=
{
onCancel
}
>
{
t
(
'关闭'
)
}
</
Button
>
<
Button
<
Button
icon=
{
<
IconRefresh
/>
}
icon=
{
<
IconRefresh
/>
}
onClick=
{
loadKeyStatus
}
onClick=
{
()
=>
loadKeyStatus
(
currentPage
,
pageSize
)
}
loading=
{
loading
}
loading=
{
loading
}
>
>
{
t
(
'刷新'
)
}
{
t
(
'刷新'
)
}
...
@@ -325,7 +374,7 @@ const MultiKeyManageModal = ({
...
@@ -325,7 +374,7 @@ const MultiKeyManageModal = ({
<
div
>
<
div
>
<
Text
>
<
Text
>
{
t
(
'总共
{{
total
}}
个密钥,
{{
enabled
}}
个已启用,
{{
manual
}}
个手动禁用,
{{
auto
}}
个自动禁用'
,
{
{
t
(
'总共
{{
total
}}
个密钥,
{{
enabled
}}
个已启用,
{{
manual
}}
个手动禁用,
{{
auto
}}
个自动禁用'
,
{
total
:
total
Count
,
total
:
total
,
enabled
:
enabledCount
,
enabled
:
enabledCount
,
manual
:
manualDisabledCount
,
manual
:
manualDisabledCount
,
auto
:
autoDisabledCount
auto
:
autoDisabledCount
...
@@ -345,15 +394,63 @@ const MultiKeyManageModal = ({
...
@@ -345,15 +394,63 @@ const MultiKeyManageModal = ({
{
/* Key Status Table */
}
{
/* Key Status Table */
}
<
Spin
spinning=
{
loading
}
>
<
Spin
spinning=
{
loading
}
>
{
keyStatusList
.
length
>
0
?
(
{
keyStatusList
.
length
>
0
?
(
<
Table
<>
columns=
{
columns
}
<
Table
dataSource=
{
keyStatusList
}
columns=
{
columns
}
pagination=
{
false
}
dataSource=
{
keyStatusList
}
size=
'small'
pagination=
{
false
}
bordered
size=
'small'
rowKey=
'index'
bordered
style=
{
{
maxHeight
:
'400px'
,
overflow
:
'auto'
}
}
rowKey=
'index'
/>
style=
{
{
marginBottom
:
'16px'
}
}
/>
{
/* Pagination */
}
{
total
>
0
&&
(
<
div
style=
{
{
display
:
'flex'
,
justifyContent
:
'space-between'
,
alignItems
:
'center'
}
}
>
<
Text
type=
'quaternary'
style=
{
{
fontSize
:
'12px'
}
}
>
{
t
(
'显示第
{{
start
}}
-
{{
end
}}
条,共
{{
total
}}
条'
,
{
start
:
(
currentPage
-
1
)
*
pageSize
+
1
,
end
:
Math
.
min
(
currentPage
*
pageSize
,
total
),
total
:
total
})
}
</
Text
>
<
div
style=
{
{
display
:
'flex'
,
alignItems
:
'center'
,
gap
:
'12px'
}
}
>
<
Text
type=
'quaternary'
style=
{
{
fontSize
:
'12px'
}
}
>
{
t
(
'每页显示'
)
}
:
</
Text
>
<
Select
value=
{
pageSize
}
onChange=
{
handlePageSizeChange
}
size=
'small'
style=
{
{
width
:
'80px'
}
}
>
<
Select
.
Option
value=
{
50
}
>
50
</
Select
.
Option
>
<
Select
.
Option
value=
{
100
}
>
100
</
Select
.
Option
>
<
Select
.
Option
value=
{
200
}
>
500
</
Select
.
Option
>
<
Select
.
Option
value=
{
1000
}
>
1000
</
Select
.
Option
>
</
Select
>
<
Pagination
current=
{
currentPage
}
total=
{
total
}
pageSize=
{
pageSize
}
showSizeChanger=
{
false
}
showQuickJumper
size=
'small'
onChange=
{
handlePageChange
}
showTotal=
{
(
total
,
range
)
=>
t
(
'第
{{
current
}}
/ {{total}} 页', {
current
:
currentPage
,
total
:
totalPages
})
}
/>
</
div
>
</
div
>
)
}
</>
)
:
(
)
:
(
!
loading
&&
(
!
loading
&&
(
<
Empty
<
Empty
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment