feat: Support log management (#668)

This commit is contained in:
王嘉豪
2025-12-24 14:16:40 +08:00
committed by GitHub
parent 9428875e25
commit d04395813a
24 changed files with 938 additions and 18 deletions
+1
View File
@@ -9,6 +9,7 @@ from apps.system.api import login, user, aimodel, workspace, assistant, paramete
from apps.terminology.api import terminology
from apps.settings.api import base
api_router = APIRouter()
api_router.include_router(login.router)
api_router.include_router(user.router)
+4 -3
View File
@@ -81,14 +81,15 @@ async def rename(session: SessionDep, chat: RenameChat):
)
@router.delete("/{chart_id}", response_model=str, summary=f"{PLACEHOLDER_PREFIX}delete_chat")
@router.delete("/{chart_id}/{brief}", response_model=str, summary=f"{PLACEHOLDER_PREFIX}delete_chat")
@system_log(LogConfig(
operation_type=OperationType.DELETE_QA,
operation_detail=OperationDetails.DELETE_QA_DETAILS,
module=OperationModules.QA,
resource_id_expr="chart_id"
resource_id_expr="chart_id",
remark_expr="brief"
))
async def delete(session: SessionDep, chart_id: int):
async def delete(session: SessionDep, chart_id: int, brief: str):
try:
return delete_chat(session=session, chart_id=chart_id)
except Exception as e:
+4 -3
View File
@@ -30,14 +30,15 @@ async def update_resource_api(session: SessionDep, user: CurrentUser, dashboard:
return update_resource(session=session, user=user, dashboard=dashboard)
@router.delete("/delete_resource/{resource_id}")
@router.delete("/delete_resource/{resource_id}/{name}")
@system_log(LogConfig(
operation_type=OperationType.DELETE_DASHBOARD,
operation_detail=OperationDetails.DELETE_DASHBOARD_DETAILS,
module=OperationModules.DASHBOARD,
resource_id_expr="resource_id"
resource_id_expr="resource_id",
remark_expr="name"
))
async def delete_resource_api(session: SessionDep, resource_id: str):
async def delete_resource_api(session: SessionDep, resource_id: str, name: str):
return delete_resource(session, resource_id)
+5 -3
View File
@@ -30,6 +30,7 @@ from ..models.datasource import CoreDatasource, CreateDatasource, TableObj, Core
TableSchemaResponse, ColumnSchemaResponse, PreviewResponse
from sqlbot_xpack.audit.models.log_model import OperationType, OperationDetails, OperationModules
from sqlbot_xpack.audit.schemas.logger_decorator import system_log, LogConfig
router = APIRouter(tags=["Datasource"], prefix="/datasource")
path = settings.EXCEL_PATH
@@ -107,15 +108,16 @@ async def update(session: SessionDep, trans: Trans, user: CurrentUser, ds: CoreD
return await asyncio.to_thread(inner)
@router.post("/delete/{id}", response_model=None, summary=f"{PLACEHOLDER_PREFIX}ds_delete")
@router.post("/delete/{id}/{name}", response_model=None, summary=f"{PLACEHOLDER_PREFIX}ds_delete")
@require_permissions(permission=SqlbotPermission(type='ds', keyExpression="id"))
@system_log(LogConfig(
operation_type=OperationType.DELETE_DATASOURCE,
operation_detail=OperationDetails.DELETE_DATASOURCE_DETAILS,
module=OperationModules.DATASOURCE,
resource_id_expr="id"
resource_id_expr="id",
remark_expr="name"
))
async def delete(session: SessionDep, id: int = Path(..., description=f"{PLACEHOLDER_PREFIX}ds_id")):
async def delete(session: SessionDep, id: int = Path(..., description=f"{PLACEHOLDER_PREFIX}ds_id"), name: str = None):
return delete_ds(session, id)
+43
View File
@@ -121,5 +121,48 @@
},
"i18n_authentication": {
"record_not_exist": "{msg} record does not exist. Please save it first!"
},
"i18n_audit": {
"success": "Success",
"failed": "Failed",
"create_qa": "Create Q&A",
"delete_qa": "Delete Q&A",
"create_datasource": "Create Datasource",
"update_datasource": "Edit Datasource",
"delete_datasource": "Delete Datasource",
"create_dashboard": "Create Dashboard",
"update_dashboard": "Edit Dashboard",
"delete_dashboard": "Delete Dashboard",
"setting_create_user": "Add Member",
"setting_delete_user": "Remove Member",
"setting_create_rule": "Add Rule Group",
"setting_delete_rule": "Remove Rule Group",
"setting_update_rule": "Configure Rules",
"setting_update_rule_user": "Configure Users",
"setting_create_terminology": "Create Terminology",
"create_qa_details": "Create Q&A【{resource_name}】",
"delete_qa_details": "Delete Q&A【{resource_name}】",
"create_datasource_details": "Create Datasource【{resource_name}】",
"update_datasource_details": "Edit Datasource【{resource_name}】",
"delete_datasource_details": "Delete Datasource【{resource_name}】",
"create_dashboard_details": "Create Dashboard【{resource_name}】",
"update_dashboard_details": "Edit Dashboard【{resource_name}】",
"delete_dashboard_details": "Delete Dashboard【{resource_name}】",
"setting_create_user_details": "Add Member【{resource_name}】",
"setting_delete_user_details": "Remove Member【{resource_name}】",
"setting_create_rule_details": "Add Rule Group【{resource_name}】",
"setting_delete_rule_details": "Remove Rule Group【{resource_name}】",
"setting_update_rule_details": "Configure Rules【{resource_name}】",
"setting_update_rule_user_details": "Configure Users【{resource_name}】",
"setting_create_terminology_details": "Create Terminology【{resource_name}】",
"system_log": "System Log",
"operation_type_name": "Operation Type",
"operation_detail_info": "Operation Details",
"user_name": "Operating User",
"oid_name": "Workspace",
"operation_status_name": "Operation Status",
"error_message": "Error Message",
"ip_address": "IP Address",
"create_time": "Operation Time"
}
}
+43
View File
@@ -121,5 +121,48 @@
},
"i18n_authentication": {
"record_not_exist": "{msg} 기록이 존재하지 않습니다. 먼저 저장해 주세요!"
},
"i18n_audit": {
"success": "성공",
"failed": "실패",
"create_qa": "Q&A 생성",
"delete_qa": "Q&A 삭제",
"create_datasource": "데이터 소스 생성",
"update_datasource": "데이터 소스 편집",
"delete_datasource": "데이터 소스 삭제",
"create_dashboard": "대시보드 생성",
"update_dashboard": "대시보드 편집",
"delete_dashboard": "대시보드 삭제",
"setting_create_user": "멤버 추가",
"setting_delete_user": "멤버 제거",
"setting_create_rule": "규칙 그룹 추가",
"setting_delete_rule": "규칙 그룹 제거",
"setting_update_rule": "규칙 설정",
"setting_update_rule_user": "사용자 설정",
"setting_create_terminology": "용어 생성",
"create_qa_details": "Q&A 생성【{resource_name}】",
"delete_qa_details": "Q&A 삭제【{resource_name}】",
"create_datasource_details": "데이터 소스 생성【{resource_name}】",
"update_datasource_details": "데이터 소스 편집【{resource_name}】",
"delete_datasource_details": "데이터 소스 삭제【{resource_name}】",
"create_dashboard_details": "대시보드 생성【{resource_name}】",
"update_dashboard_details": "대시보드 편집【{resource_name}】",
"delete_dashboard_details": "대시보드 삭제【{resource_name}】",
"setting_create_user_details": "멤버 추가【{resource_name}】",
"setting_delete_user_details": "멤버 제거【{resource_name}】",
"setting_create_rule_details": "규칙 그룹 추가【{resource_name}】",
"setting_delete_rule_details": "규칙 그룹 제거【{resource_name}】",
"setting_update_rule_details": "규칙 설정【{resource_name}】",
"setting_update_rule_user_details": "사용자 설정【{resource_name}】",
"setting_create_terminology_details": "용어 생성【{resource_name}】",
"system_log": "시스템 로그",
"operation_type_name": "작업 유형",
"operation_detail_info": "작업 상세 정보",
"user_name": "작업 사용자",
"oid_name": "작업 공간",
"operation_status_name": "작업 상태",
"error_message": "오류 메시지",
"ip_address": "IP 주소",
"create_time": "작업 시간"
}
}
+44 -1
View File
@@ -121,5 +121,48 @@
},
"i18n_authentication": {
"record_not_exist": "{msg} 记录不存在,请先保存!"
},
"i18n_audit": {
"success": "成功",
"failed": "失败",
"create_qa": "新建问数",
"delete_qa": "删除问数",
"create_datasource": "新建数据源",
"update_datasource": "编辑数据源",
"delete_datasource": "删除数据源",
"create_dashboard": "新建仪表板",
"update_dashboard": "编辑仪表板",
"delete_dashboard": "删除仪表板",
"setting_create_user": "添加成员",
"setting_delete_user": "移除成员",
"setting_create_rule": "添加规则组",
"setting_delete_rule": "移除规则组",
"setting_update_rule": "设置规则",
"setting_update_rule_user": "设置用户",
"setting_create_terminology": "新建术语",
"create_qa_details": "新建问数【{resource_name}】",
"delete_qa_details": "删除问数【{resource_name}】",
"create_datasource_details": "新建数据源【{resource_name}】",
"update_datasource_details": "编辑数据源【{resource_name}】",
"delete_datasource_details": "删除数据源【{resource_name}】",
"create_dashboard_details": "新建仪表板【{resource_name}】",
"update_dashboard_details": "编辑仪表板【{resource_name}】",
"delete_dashboard_details": "删除仪表板【{resource_name}】",
"setting_create_user_details": "添加成员【{resource_name}】",
"setting_delete_user_details": "移除成员【{resource_name}】",
"setting_create_rule_details": "添加规则组【{resource_name}】",
"setting_delete_rule_details": "移除规则组【{resource_name}】",
"setting_update_rule_details": "设置规则【{resource_name}】",
"setting_update_rule_user_details": "设置用户【{resource_name}】",
"setting_create_terminology_details": "新建术语【{resource_name}】",
"system_log": "操作日志",
"operation_type_name": "操作类型",
"operation_detail_info": "操作详情",
"user_name": "操作用户",
"oid_name": "工作空间",
"operation_status_name": "操作状态",
"error_message": "错误信息",
"ip_address": "IP 地址",
"create_time": "操作时间"
}
}
}
+1 -1
View File
@@ -83,11 +83,11 @@ url = "https://test.pypi.org/simple"
explicit = true
[tool.uv.sources]
sqlbot-xpack = { index = "testpypi" }
torch = [
{ index = "pytorch-cpu", extra = "cpu" },
{ index = "pytorch-cu128", extra = "cu128" },
]
sqlbot-xpack = { path = "../../sqlbot-xpack" }
[tool.uv]
conflicts = [
+12
View File
@@ -0,0 +1,12 @@
import { request } from '@/utils/request'
export const audit = {
getList: (pageNum: any, pageSize: any, params: any) =>
request.get(`/system/audit/page/${pageNum}/${pageSize}${params}`),
export2Excel: (params: any) =>
request.get(`/system/audit/export`, {
params,
responseType: 'blob',
requestOptions: { customError: true },
}),
}
+2 -2
View File
@@ -324,8 +324,8 @@ export const chatApi = {
renameChat: (chat_id: number | undefined, brief: string): Promise<string> => {
return request.post('/chat/rename', { id: chat_id, brief: brief })
},
deleteChat: (id: number | undefined): Promise<string> => {
return request.delete(`/chat/${id}`)
deleteChat: (id: number | undefined, brief: string): Promise<string> => {
return request.delete(`/chat/${id}/${brief}`)
},
analysis: (record_id: number | undefined, controller?: AbortController) => {
return request.fetchStream(`/chat/record/${record_id}/analysis`, {}, controller)
+1 -1
View File
@@ -9,6 +9,6 @@ export const dashboardApi = {
update_canvas: (params: any) => request.post('/dashboard/update_canvas', params),
check_name: (params: any) => request.post('/dashboard/check_name', params),
delete_resource: (params: any) =>
request.delete(`/dashboard/delete_resource/${params.id}`, params),
request.delete(`/dashboard/delete_resource/${params.id}/${params.name}`, params),
move_resource: (params: any) => request.delete(`/dashboard/move_resource/${params.id}`, params),
}
+1 -1
View File
@@ -8,7 +8,7 @@ export const datasourceApi = {
add: (data: any) => request.post('/datasource/add', data),
list: () => request.get('/datasource/list'),
update: (data: any) => request.post('/datasource/update', data),
delete: (id: number) => request.post(`/datasource/delete/${id}`),
delete: (id: number, name: str) => request.post(`/datasource/delete/${id}/${name}`),
getTables: (id: number) => request.post(`/datasource/getTables/${id}`),
getTablesByConf: (data: any) => request.post('/datasource/getTablesByConf', data),
getFields: (id: number, table_name: string) =>
+11
View File
@@ -0,0 +1,11 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_8570_348)">
<path d="M8.00002 15.3334C3.95002 15.3334 0.666687 12.05 0.666687 8.00005C0.666687 3.95005 3.95002 0.666718 8.00002 0.666718C12.05 0.666718 15.3334 3.95005 15.3334 8.00005C15.3334 12.05 12.05 15.3334 8.00002 15.3334Z" fill="#F54A45"/>
<path d="M6.01683 5.07416L8.00016 7.05749L9.98349 5.07416C10.046 5.01167 10.1308 4.97656 10.2192 4.97656C10.3075 4.97656 10.3923 5.01167 10.4548 5.07416L10.9262 5.54549C10.9886 5.608 11.0238 5.69277 11.0238 5.78116C11.0238 5.86955 10.9886 5.95432 10.9262 6.01682L8.94283 8.00016L10.9262 9.98349C10.9886 10.046 11.0238 10.1308 11.0238 10.2192C11.0238 10.3075 10.9886 10.3923 10.9262 10.4548L10.4548 10.9262C10.3923 10.9886 10.3075 11.0238 10.2192 11.0238C10.1308 11.0238 10.046 10.9886 9.98349 10.9262L8.00016 8.94282L6.01683 10.9262C5.95432 10.9886 5.86955 11.0238 5.78116 11.0238C5.69277 11.0238 5.608 10.9886 5.54549 10.9262L5.07416 10.4548C5.01167 10.3923 4.97656 10.3075 4.97656 10.2192C4.97656 10.1308 5.01167 10.046 5.07416 9.98349L7.05749 8.00016L5.07416 6.01682C5.01167 5.95432 4.97656 5.86955 4.97656 5.78116C4.97656 5.69277 5.01167 5.608 5.07416 5.54549L5.54549 5.07416C5.608 5.01167 5.69277 4.97656 5.78116 4.97656C5.86955 4.97656 5.95432 5.01167 6.01683 5.07416Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_8570_348">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+15
View File
@@ -0,0 +1,15 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_7974_67866)">
<g clip-path="url(#clip1_7974_67866)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.00002 14C11.3137 14 14 11.3137 14 7.99999C14 4.68628 11.3137 1.99999 8.00002 1.99999C4.68631 1.99999 2.00002 4.68628 2.00002 7.99999C2.00002 11.3137 4.68631 14 8.00002 14ZM8.00002 15.3333C3.94993 15.3333 0.666687 12.0501 0.666687 7.99999C0.666687 3.9499 3.94993 0.666656 8.00002 0.666656C12.0501 0.666656 15.3334 3.9499 15.3334 7.99999C15.3334 12.0501 12.0501 15.3333 8.00002 15.3333ZM7.66669 10.6667H8.33335C8.51745 10.6667 8.66669 10.8159 8.66669 11V11.6667C8.66669 11.8507 8.51745 12 8.33335 12H7.66669C7.48259 12 7.33335 11.8507 7.33335 11.6667V11C7.33335 10.8159 7.48259 10.6667 7.66669 10.6667ZM5.36659 6.1878C5.37506 6.11328 5.38374 6.0513 5.39262 6.00184C5.52258 5.27811 5.85171 4.74588 6.38003 4.40513C6.7938 4.13504 7.30223 3.99999 7.90534 3.99999C8.69781 3.99999 9.35614 4.1919 9.88036 4.57572C10.4046 4.95954 10.6667 5.52815 10.6667 6.28158C10.6667 6.74359 10.5527 7.13274 10.3248 7.44903C10.1916 7.64094 9.93559 7.88616 9.55689 8.18469L9.18345 8.47788C8.98007 8.63781 8.84508 8.82438 8.77845 9.03762C8.76032 9.09581 8.74439 9.20399 8.73065 9.36215C8.7157 9.53443 8.57149 9.66666 8.39857 9.66666H7.6523C7.4682 9.66665 7.31896 9.51742 7.31896 9.33332C7.31896 9.32307 7.31944 9.31282 7.32038 9.30261C7.36331 8.83868 7.40927 8.54511 7.45827 8.42191C7.54944 8.19268 7.78437 7.92881 8.16307 7.63028L8.54703 7.32642C8.67326 7.23047 9.32373 6.77242 9.32373 6.36688C9.32373 5.96133 9.25308 5.81217 9.04946 5.58959C8.84584 5.36702 8.38748 5.29538 7.97372 5.29538C7.56697 5.29538 7.21938 5.4034 7.04932 5.67705C6.97055 5.80379 6.90615 5.939 6.85969 6.07695C6.84142 6.13122 6.82522 6.1995 6.81111 6.28179C6.78367 6.44181 6.64493 6.55878 6.48258 6.55878H5.6978C5.5137 6.55879 5.36446 6.40955 5.36446 6.22545C5.36446 6.21287 5.36517 6.2003 5.36659 6.1878Z" fill="#646A73"/>
</g>
</g>
<defs>
<clipPath id="clip0_7974_67866">
<rect width="16" height="16" rx="4" fill="white"/>
</clipPath>
<clipPath id="clip1_7974_67866">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+11
View File
@@ -0,0 +1,11 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_8570_412)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.666687 7.99999C0.666687 12.0505 3.9495 15.3333 8.00002 15.3333C12.0505 15.3333 15.3334 12.0505 15.3334 7.99999C15.3334 3.94947 12.0505 0.666656 8.00002 0.666656C3.9495 0.666656 0.666687 3.94947 0.666687 7.99999Z" fill="#34C724"/>
<path d="M10.7621 5.98431L7.22639 9.51998L5.49305 7.78665C5.43054 7.72416 5.34578 7.68905 5.25739 7.68905C5.169 7.68905 5.08423 7.72416 5.02172 7.78665L4.55072 8.25831C4.48823 8.32082 4.45312 8.40559 4.45312 8.49398C4.45312 8.58237 4.48823 8.66714 4.55072 8.72964L6.99072 11.17C7.05323 11.2325 7.138 11.2676 7.22639 11.2676C7.31477 11.2676 7.39954 11.2325 7.46205 11.17L11.7047 6.92731C11.7672 6.8648 11.8023 6.78003 11.8023 6.69165C11.8023 6.60326 11.7672 6.51849 11.7047 6.45598L11.2334 5.98431C11.1709 5.92182 11.0861 5.88672 10.9977 5.88672C10.9093 5.88672 10.8246 5.92182 10.7621 5.98431Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_8570_412">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -55,6 +55,7 @@ const routerList = computed(() => {
!route.path.includes('prompt') &&
!route.path.includes('permission') &&
!route.path.includes('preview') &&
!route.path.includes('audit') &&
route.path !== '/login' &&
route.path !== '/admin-login' &&
!route.path.includes('/system') &&
+34
View File
@@ -822,6 +822,40 @@
"modelType": {
"llm": "Large Language Model"
},
"audit": {
"system_log": "System Log",
"search_log": "Search Log",
"operation_type": "Operation Type",
"operation_details": "Operation Details",
"operation_user_name": "Operating User",
"operation_status": "Operation Status",
"user_name": "User Name",
"oid_name": "Workspace",
"ip_address": "IP Address",
"create_time": "Creation Time",
"no_log": "No Logs Available",
"all_236_terms": "Export all {msg} log entries?",
"export_hint": "Export all logs?",
"export": "Export",
"success": "Success",
"failed": "Failed",
"failed_info": "Operation Failure Details",
"create_qa": "Create Q&A",
"delete_qa": "Delete Q&A",
"create_datasource": "Create Datasource",
"update_datasource": "Edit Datasource",
"delete_datasource": "Delete Datasource",
"create_dashboard": "Create Dashboard",
"update_dashboard": "Edit Dashboard",
"delete_dashboard": "Delete Dashboard",
"setting_create_user": "Add Member",
"setting_delete_user": "Remove Member",
"setting_create_rule": "Add Rule Group",
"setting_delete_rule": "Remove Rule Group",
"setting_update_rule": "Configure Rules",
"setting_update_rule_user": "Configure Users",
"setting_create_terminology": "Create Terminology"
}
"api_key": {
"info_tips": "The API Key is your credential for accessing the SQLBot API, with full permissions for your account. Please keep it secure! Do not disclose the API Key in any public channels to avoid security risks from unauthorized use.",
"create": "Create",
+34
View File
@@ -822,6 +822,40 @@
"modelType": {
"llm": "대형 언어 모델"
},
"audit": {
"system_log": "시스템 로그",
"search_log": "검색 로그",
"operation_type": "작업 유형",
"operation_details": "작업 상세",
"operation_user_name": "작업 사용자",
"operation_status": "작업 상태",
"user_name": "사용자 이름",
"oid_name": "작업 공간",
"ip_address": "IP 주소",
"create_time": "생성 시간",
"no_log": "로그 없음",
"all_236_terms": "전체 {msg}개의 로그를 내보내시겠습니까?",
"export_hint": "모든 로그를 내보내시겠습니까?",
"export": "내보내기",
"success": "성공",
"failed": "실패",
"failed_info": "작업 실패 상세",
"create_qa": "Q&A 생성",
"delete_qa": "Q&A 삭제",
"create_datasource": "데이터 소스 생성",
"update_datasource": "데이터 소스 편집",
"delete_datasource": "데이터 소스 삭제",
"create_dashboard": "대시보드 생성",
"update_dashboard": "대시보드 편집",
"delete_dashboard": "대시보드 삭제",
"setting_create_user": "멤버 추가",
"setting_delete_user": "멤버 제거",
"setting_create_rule": "규칙 그룹 추가",
"setting_delete_rule": "규칙 그룹 제거",
"setting_update_rule": "규칙 설정",
"setting_update_rule_user": "사용자 설정",
"setting_create_terminology": "용어 생성"
}
"api_key": {
"info_tips": "API 키는 SQLBot API에 액세스하는 비밀키로 계정의 모든 권한을 갖고 있습니다. 꼭 안전하게 보관하세요! 외부 채널에 어떠한 방식으로도 API 키를 공개하지 마시고, 타인이 악용하여 보안 위협이 발생하는 것을 방지하세요.",
"create": "생성",
+34
View File
@@ -823,6 +823,40 @@
"modelType": {
"llm": "大语言模型"
},
"audit": {
"system_log": "操作日志",
"search_log": "搜索日志",
"operation_type": "操作类型",
"operation_details": "操作详情",
"operation_user_name": "操作用户",
"operation_status": "操作状态",
"user_name": "操作日志",
"oid_name": "工作空间",
"ip_address": "IP 地址",
"create_time": "创建时间",
"no_log": "暂无日志",
"all_236_terms": "是否导出全部 {msg} 条日志?",
"export_hint": "是否导出全部日志?",
"export": "导出",
"success": "成功",
"failed": "失败",
"failed_info": "操作失败详情",
"create_qa": "新建问数",
"delete_qa": "删除问数",
"create_datasource": "新建数据源",
"update_datasource": "编辑数据源",
"delete_datasource": "删除数据源",
"create_dashboard": "新建仪表板",
"update_dashboard": "编辑仪表板",
"delete_dashboard": "删除仪表板",
"setting_create_user": "添加成员",
"setting_delete_user": "移除成员",
"setting_create_rule": "添加规则组",
"setting_delete_rule": "移除规则组",
"setting_update_rule": "设置规则",
"setting_update_rule_user": "设置用户",
"setting_create_terminology": "新建术语"
}
"api_key": {
"info_tips": "API Key 是您访问 SQLBot API 的密钥,具有账户的完全权限,请您务必妥善保管!不要以任何方式公开 API Key 到外部渠道,避免被他人利用造成安全威胁。",
"create": "创建",
+7
View File
@@ -18,6 +18,7 @@ import Member from '@/views/system/member/index.vue'
import Professional from '@/views/system/professional/index.vue'
import Training from '@/views/system/training/index.vue'
import Prompt from '@/views/system/prompt/index.vue'
import Audit from '@/views/system/audit/index.vue'
import Appearance from '@/views/system/appearance/index.vue'
import Parameter from '@/views/system/parameter/index.vue'
import Authentication from '@/views/system/authentication/index.vue'
@@ -130,6 +131,12 @@ export const routes = [
component: Prompt,
meta: { title: t('prompt.customize_prompt_words') },
},
{
path: '/set/audit',
name: 'audit',
component: Audit,
meta: { title: t('audit.system_log') },
},
],
},
{
+1 -1
View File
@@ -122,7 +122,7 @@ function handleCommand(command: string | number | object, chat: Chat) {
}).then(() => {
_loading.value = true
chatApi
.deleteChat(chat.id)
.deleteChat(chat.id, chat.brief)
.then(() => {
ElMessage({
type: 'success',
@@ -230,7 +230,7 @@ const operation = (opt: string, data: SQTreeNode) => {
autofocus: false,
showClose: false,
}).then(() => {
dashboardApi.delete_resource({ id: data.id }).then(() => {
dashboardApi.delete_resource({ id: data.id, name: data.name }).then(() => {
ElMessage.success(t('dashboard.delete_success'))
getTree()
dashboardStore.canvasDataInit()
+1 -1
View File
@@ -159,7 +159,7 @@ const deleteHandler = (item: any) => {
''
),
}).then(() => {
datasourceApi.delete(item.id).then(() => {
datasourceApi.delete(item.id, item.name).then(() => {
ElMessage({
type: 'success',
message: t('dashboard.delete_success'),
+627
View File
@@ -0,0 +1,627 @@
<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue'
import icon_export_outlined from '@/assets/svg/icon_export_outlined.svg'
import { formatTimestamp } from '@/utils/date'
import EmptyBackground from '@/views/dashboard/common/EmptyBackground.vue'
import { useI18n } from 'vue-i18n'
import { convertFilterText, FilterText } from '@/components/filter-text'
import { DrawerMain } from '@/components/drawer-main'
import iconFilter from '@/assets/svg/icon-filter_outlined.svg'
import { audit } from '@/api/audit.ts'
import { workspaceList } from '@/api/workspace.ts'
import { userApi } from '@/api/auth.ts'
import icon_success from '@/assets/svg/icon_success.svg'
import icon_error from '@/assets/svg/icon_error.svg'
import icon_issue from '@/assets/svg/icon_issue.svg'
const { t } = useI18n()
const multipleSelectionAll = ref<any[]>([])
const keywords = ref('')
const oldKeywords = ref('')
const searchLoading = ref(false)
const drawerMainRef = ref()
onMounted(() => {
search()
initWorkspace()
initUsers()
})
const multipleTableRef = ref()
const fieldList = ref<any>([])
const pageInfo = reactive({
currentPage: 1,
pageSize: 10,
total: 0,
})
const curErrorInfo = ref()
const state = reactive<any>({
conditions: [],
filterTexts: [],
})
const exportExcel = () => {
ElMessageBox.confirm(t('audit.all_236_terms', { msg: pageInfo.total }), {
confirmButtonType: 'primary',
confirmButtonText: t('audit.export'),
cancelButtonText: t('common.cancel'),
customClass: 'confirm-no_icon',
autofocus: false,
}).then(() => {
searchLoading.value = true
audit
.export2Excel(configParams())
.then((res) => {
const blob = new Blob([res], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
})
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = `${t('audit.system_log')}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
})
.catch(async (error) => {
if (error.response) {
try {
let text = await error.response.data.text()
try {
text = JSON.parse(text)
} finally {
ElMessage({
message: text,
type: 'error',
showClose: true,
})
}
} catch (e) {
console.error('Error processing error response:', e)
}
} else {
console.error('Other error:', error)
ElMessage({
message: error,
type: 'error',
showClose: true,
})
}
})
.finally(() => {
searchLoading.value = false
})
})
}
const toggleRowLoading = ref(false)
const configParams = () => {
let str = ''
if (keywords.value) {
str += `name=${keywords.value}`
}
state.conditions.forEach((ele: any) => {
ele.value.forEach((itx: any) => {
str += str ? `__${itx}` : `${ele.field}=${itx}`
})
})
if (str.length) {
str = `?${str}`
}
return str
}
const search = () => {
searchLoading.value = true
oldKeywords.value = keywords.value
audit
.getList(pageInfo.currentPage, pageInfo.pageSize, configParams())
.then((res) => {
toggleRowLoading.value = true
fieldList.value = res.data
pageInfo.total = res.total_count
searchLoading.value = false
})
.finally(() => {
searchLoading.value = false
})
}
const handleSizeChange = (val: number) => {
pageInfo.currentPage = 1
pageInfo.pageSize = val
search()
}
const handleCurrentChange = (val: number) => {
pageInfo.currentPage = val
search()
}
const rowInfoDialog = ref(false)
const onErrorInfoOpen = (info: string) => {
rowInfoDialog.value = true
curErrorInfo.value = info
}
const onErrorInfoClose = () => {
rowInfoDialog.value = false
}
const operationTypeOptions = [
{ id: 'create_qa', name: t('audit.create_qa') },
{ id: 'delete_qa', name: t('audit.delete_qa') },
{ id: 'create_datasource', name: t('audit.create_datasource') },
{ id: 'update_datasource', name: t('audit.update_datasource') },
{ id: 'delete_datasource', name: t('audit.delete_datasource') },
{ id: 'create_dashboard', name: t('audit.create_dashboard') },
{ id: 'update_dashboard', name: t('audit.update_dashboard') },
{ id: 'delete_dashboard', name: t('audit.delete_dashboard') },
{ id: 'setting_create_user', name: t('audit.setting_create_user') },
{ id: 'setting_delete_user', name: t('audit.setting_delete_user') },
{ id: 'setting_create_rule', name: t('audit.setting_create_rule') },
{ id: 'setting_delete_rule', name: t('audit.setting_delete_rule') },
{ id: 'setting_update_rule', name: t('audit.setting_update_rule') },
{ id: 'setting_update_rule_user', name: t('audit.setting_update_rule_user') },
{ id: 'setting_create_terminology', name: t('audit.setting_create_terminology') },
]
const filterOption = ref<any[]>([
{
type: 'select',
option: operationTypeOptions,
field: 'opt_type_list',
title: t('audit.operation_type'),
operate: 'in',
property: { placeholder: t('common.empty') + t('audit.operation_type') },
},
{
type: 'select',
option: [],
field: 'uid_list',
title: t('audit.operation_user_name'),
operate: 'in',
property: { placeholder: t('common.empty') + t('audit.operation_user_name') },
},
{
type: 'select',
option: [],
field: 'oid_list',
title: t('audit.operation_user_name'),
operate: 'in',
property: { placeholder: t('common.empty') + t('audit.oid_name') },
},
{
type: 'enum',
option: [
{ id: 'success', name: t('audit.success') },
{ id: 'failed', name: t('audit.failed') },
],
field: 'log_status',
title: t('user.user_status'),
operate: 'in',
},
])
const fillFilterText = () => {
const textArray = state.conditions?.length
? convertFilterText(state.conditions, filterOption.value)
: []
state.filterTexts = [...textArray]
Object.assign(state.filterTexts, textArray)
}
const searchCondition = (conditions: any) => {
state.conditions = conditions
fillFilterText()
search()
drawerMainClose()
}
const clearFilter = (params?: number) => {
let index = params ? params : 0
if (isNaN(index)) {
state.filterTexts = []
} else {
state.filterTexts.splice(index, 1)
}
drawerMainRef.value.clearFilter(index)
}
const drawerMainOpen = async () => {
drawerMainRef.value.init()
}
const drawerMainClose = () => {
drawerMainRef.value.close()
}
const initWorkspace = () => {
workspaceList().then((res) => {
filterOption.value[2].option = [...res]
})
}
const initUsers = () => {
userApi.pager(1, 10000).then((res: any) => {
filterOption.value[1].option = [{ id: 1, name: 'Administrator' }, ...res.items]
})
}
</script>
<template>
<div v-loading="searchLoading" class="professional">
<div class="tool-left">
<span class="page-title">{{ $t('audit.system_log') }}</span>
<div class="tool-row">
<el-button secondary @click="exportExcel">
<template #icon>
<icon_export_outlined />
</template>
{{ $t('professional.export_all') }}
</el-button>
<el-button class="no-margin" secondary @click="drawerMainOpen">
<template #icon>
<iconFilter></iconFilter>
</template>
{{ $t('user.filter') }}
</el-button>
</div>
</div>
<div
v-if="!searchLoading"
class="table-content"
:class="multipleSelectionAll?.length && 'show-pagination_height'"
>
<filter-text
:total="pageInfo.total"
:filter-texts="state.filterTexts"
@clear-filter="clearFilter"
/>
<div class="preview-or-schema">
<el-table ref="multipleTableRef" :data="fieldList" style="width: 100%">
<el-table-column prop="word" :label="$t('audit.operation_type')" width="120">
<template #default="scope">
{{ scope.row.operation_type_name }}
</template>
</el-table-column>
<el-table-column :label="$t('audit.operation_details')" min-width="200"
><template #default="scope">
{{ scope.row.operation_detail_info }}
</template>
</el-table-column>
<el-table-column :label="$t('audit.operation_user_name')" min-width="120"
><template #default="scope">
{{ scope.row.user_name }}
</template>
</el-table-column>
<el-table-column prop="word" :label="$t('audit.oid_name')" width="120">
<template #default="scope">
{{ scope.row.oid_name }}
</template>
</el-table-column>
<el-table-column prop="word" :label="$t('audit.operation_status')" width="100">
<template #default="scope">
<div style="display: flex; align-items: center">
<el-icon size="16" style="margin-right: 4px">
<icon_success v-if="scope.row.operation_status === 'success'"></icon_success>
<icon_error v-if="scope.row.operation_status === 'failed'"></icon_error>
</el-icon>
{{ scope.row.operation_status_name }}
<el-icon
v-if="scope.row.operation_status === 'failed'"
size="16"
style="margin-left: 4px; cursor: pointer"
@click="onErrorInfoOpen(scope.row.error_message)"
>
<icon_issue></icon_issue>
</el-icon>
</div>
</template>
</el-table-column>
<el-table-column prop="word" :label="$t('audit.ip_address')" width="120">
<template #default="scope">
{{ scope.row.ip_address }}
</template>
</el-table-column>
<el-table-column prop="create_time" sortable :label="$t('audit.create_time')" width="180">
<template #default="scope">
<span>{{ formatTimestamp(scope.row.create_time, 'YYYY-MM-DD HH:mm:ss') }}</span>
</template>
</el-table-column>
<template #empty>
<EmptyBackground
v-if="!oldKeywords && !fieldList.length"
:description="$t('audit.no_log')"
img-type="noneWhite"
/>
<EmptyBackground
v-if="!!oldKeywords && !fieldList.length"
:description="$t('datasource.relevant_content_found')"
img-type="tree"
/>
</template>
</el-table>
</div>
</div>
<div v-if="fieldList.length" class="pagination-container">
<el-pagination
v-model:current-page="pageInfo.currentPage"
v-model:page-size="pageInfo.pageSize"
:page-sizes="[10, 20, 30]"
:background="true"
layout="total, sizes, prev, pager, next, jumper"
:total="pageInfo.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
<el-drawer
v-model="rowInfoDialog"
:title="$t('audit.failed_info')"
destroy-on-close
:before-close="onErrorInfoClose"
direction="btt"
size="80%"
modal-class="professional-term_drawer"
>
<div style="width: 100%; height: 100%">
{{ curErrorInfo }}
</div>
</el-drawer>
<drawer-main
ref="drawerMainRef"
:filter-options="filterOption"
@trigger-filter="searchCondition"
/>
</template>
<style lang="less" scoped>
.no-margin {
margin: 0;
}
.professional {
height: 100%;
position: relative;
.datasource-yet {
padding-bottom: 0;
height: auto;
padding-top: 200px;
}
:deep(.ed-table__cell) {
cursor: pointer;
}
.tool-left {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
.page-title {
font-weight: 500;
font-size: 20px;
line-height: 28px;
}
.tool-row {
display: flex;
align-items: center;
flex-direction: row;
gap: 8px;
}
}
.pagination-container {
display: flex;
justify-content: end;
align-items: center;
margin-top: 16px;
}
.table-content {
max-height: calc(100% - 104px);
overflow-y: auto;
&.show-pagination_height {
max-height: calc(100% - 165px);
}
.preview-or-schema {
.field-comment_d {
display: flex;
align-items: center;
min-height: 24px;
}
.notes-in_table {
max-width: 100%;
display: -webkit-box;
max-height: 66px;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3; /* 限制行数为3 */
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
}
.ed-icon {
color: #646a73;
}
.user-status-container {
display: flex;
align-items: center;
font-weight: 400;
font-size: 14px;
line-height: 22px;
height: 24px;
.ed-icon {
margin-left: 8px;
}
}
.field-comment {
height: 24px;
.ed-icon {
position: relative;
cursor: pointer;
margin-top: 4px;
&::after {
content: '';
background-color: #1f23291a;
position: absolute;
border-radius: 6px;
width: 24px;
height: 24px;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
display: none;
}
&:not(.not-allow):hover {
&::after {
display: block;
}
}
&.not-allow {
cursor: not-allowed;
}
}
.ed-icon + .ed-icon {
margin-left: 12px;
}
}
.preview-num {
margin: 12px 0;
font-weight: 400;
font-size: 14px;
line-height: 22px;
color: #646a73;
}
}
}
.bottom-select {
position: absolute;
height: 64px;
width: calc(100% + 48px);
left: -24px;
bottom: -16px;
border-top: 1px solid #1f232926;
display: flex;
background-color: #fff;
align-items: center;
padding-left: 24px;
background-color: #fff;
z-index: 10;
.danger-button {
border: 1px solid var(--ed-color-danger);
color: var(--ed-color-danger);
border-radius: var(--ed-border-radius-base);
min-width: 80px;
height: 32px;
line-height: 32px;
text-align: center;
cursor: pointer;
margin: 0 16px;
background-color: transparent;
}
.primary-button {
border: 1px solid var(--ed-color-primary);
color: var(--ed-color-primary);
border-radius: var(--ed-border-radius-base);
min-width: 80px;
height: 32px;
line-height: 32px;
text-align: center;
cursor: pointer;
margin: 0 16px;
background-color: transparent;
}
.selected {
font-weight: 400;
font-size: 14px;
line-height: 22px;
color: #646a73;
margin-right: 12px;
}
}
}
</style>
<style lang="less">
.professional-term_drawer {
.ed-form-item--label-top .ed-form-item__label {
margin-bottom: 4px;
}
.ed-form-item__label {
color: #646a73;
}
.content {
width: 100%;
line-height: 22px;
word-break: break-all;
}
}
.professional-add_drawer {
.no-error.no-error {
.ed-form-item__error {
display: none;
}
margin-bottom: 16px;
}
.ed-textarea__inner {
line-height: 22px;
}
.ed-form-item__label:has(.btn) {
padding-right: 0;
width: 100%;
}
.scrollbar-item {
display: flex;
align-items: center;
justify-content: space-between;
padding-right: 4px;
margin-bottom: 8px;
}
.synonyms-list {
position: absolute;
left: 0;
top: 0;
width: calc(100% + 4px);
height: calc(100vh - 390px);
}
.btn {
margin-left: auto;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px;
border-radius: 6px;
margin-right: -4px;
cursor: pointer;
&:hover {
background-color: #1f23291a;
}
}
}
</style>